@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.
Files changed (113) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/components.cjs.js +177 -177
  3. package/dist/components.esm.css +3 -3
  4. package/dist/hooks.cjs.js +1 -1
  5. package/dist/i18n.cjs.js +3 -3
  6. package/dist/ihabdevteam-core.cjs.js +181 -181
  7. package/dist/ihabdevteam-core.d.ts +38 -7
  8. package/dist/packages/core/src/components/Alert.js +1 -1
  9. package/dist/packages/core/src/components/Breadcrumb.js +1 -0
  10. package/dist/packages/core/src/components/Button.css +5 -0
  11. package/dist/packages/core/src/components/Button.js +3 -3
  12. package/dist/packages/core/src/components/Checkbox.js +10 -2
  13. package/dist/packages/core/src/components/CodeBox.js +2 -1
  14. package/dist/packages/core/src/components/Effect/EffectPartyPopper.js +5 -2
  15. package/dist/packages/core/src/components/Icon.js +4 -1
  16. package/dist/packages/core/src/components/IconGridMenu.js +25 -22
  17. package/dist/packages/core/src/components/LandingHeader.d.ts +1 -1
  18. package/dist/packages/core/src/components/LandingHeader.js +5 -1
  19. package/dist/packages/core/src/components/PageTitle.js +1 -0
  20. package/dist/packages/core/src/components/Pagination.js +1 -1
  21. package/dist/packages/core/src/components/Panel.css +3 -0
  22. package/dist/packages/core/src/components/Panel.d.ts +11 -1
  23. package/dist/packages/core/src/components/Panel.js +3 -3
  24. package/dist/packages/core/src/components/PanelRow.js +8 -1
  25. package/dist/packages/core/src/components/PassCard/PassCardBgIHAB.js +1 -0
  26. package/dist/packages/core/src/components/PassCard/PassCardBgLeftColorGrid.js +1 -0
  27. package/dist/packages/core/src/components/PassCard/PassCardBgSoundMove.js +1 -0
  28. package/dist/packages/core/src/components/PassCard/PassCardBgSoundMoveV1.js +1 -0
  29. package/dist/packages/core/src/components/PassCard/PassCardBgTalk.js +1 -0
  30. package/dist/packages/core/src/components/PassCard/PassCardBgTalkV1.js +1 -0
  31. package/dist/packages/core/src/components/PassCard/PassCardBgTwoCircle.js +1 -0
  32. package/dist/packages/core/src/components/Popup/DeviceSideTable.js +8 -1
  33. package/dist/packages/core/src/components/Popup/ListPopup.js +3 -1
  34. package/dist/packages/core/src/components/Popup/LoadTablePopup.js +6 -5
  35. package/dist/packages/core/src/components/Popup/Popup.css +19 -0
  36. package/dist/packages/core/src/components/Popup/Popup.d.ts +14 -1
  37. package/dist/packages/core/src/components/Popup/Popup.js +6 -5
  38. package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +6 -1
  39. package/dist/packages/core/src/components/Popup/customerDetailUtils.js +5 -3
  40. package/dist/packages/core/src/components/ProgressBar.js +5 -2
  41. package/dist/packages/core/src/components/ProgressIndicator.js +7 -1
  42. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgDark.js +1 -0
  43. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgDeep.js +1 -0
  44. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgDiagonal.js +1 -0
  45. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgDots.js +1 -0
  46. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgInstagram.js +1 -0
  47. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgMain.js +1 -0
  48. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgNaver.js +1 -0
  49. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgOutline.js +1 -0
  50. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgRadial.js +1 -0
  51. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgSoft.js +1 -0
  52. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgSoftSub.js +1 -0
  53. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgSub.js +1 -0
  54. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgSuccess.js +1 -0
  55. package/dist/packages/core/src/components/PromoBannerBg/PromoBannerBgWarm.js +1 -0
  56. package/dist/packages/core/src/components/Rating.js +3 -1
  57. package/dist/packages/core/src/components/SideNav.js +6 -3
  58. package/dist/packages/core/src/components/SplashLogo.js +1 -2
  59. package/dist/packages/core/src/components/StatChart.js +1 -1
  60. package/dist/packages/core/src/components/TextField.js +3 -1
  61. package/dist/packages/core/src/components/Tooltip/DescriptionTooltip.js +1 -0
  62. package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +6 -5
  63. package/dist/packages/core/src/components/Training/TrainingBlockButton.css +57 -0
  64. package/dist/packages/core/src/components/Training/TrainingBlockButton.js +15 -7
  65. package/dist/packages/core/src/components/Training/TrainingCardGroup.js +1 -0
  66. package/dist/packages/core/src/components/Training/TrainingPagePopupTitle.js +2 -1
  67. package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +28 -1
  68. package/dist/packages/core/src/components/Training/TrainingStepCard.js +10 -3
  69. package/dist/packages/core/src/components/Training/TrainingStepCardOrder.js +36 -26
  70. package/dist/packages/core/src/components/Training/TrainingStepOrderList.d.ts +0 -1
  71. package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +16 -1
  72. package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +7 -4
  73. package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -1
  74. package/dist/packages/core/src/components/Training/TrainingWordCard.js +21 -4
  75. package/dist/packages/core/src/components/Training/WordTablePopup.js +9 -3
  76. package/dist/packages/core/src/components/Training/currentSound.d.ts +5 -0
  77. package/dist/packages/core/src/components/Training/currentSound.js +13 -0
  78. package/dist/packages/core/src/components/Training/trainingBlockButtonTypes.d.ts +3 -1
  79. package/dist/packages/core/src/components/Training/trainingMissStats.js +9 -2
  80. package/dist/packages/core/src/components/Training/trainingPopupUtils.js +12 -1
  81. package/dist/packages/core/src/components/Training/useNoiseFade.js +4 -0
  82. package/dist/packages/core/src/components/Training/useTTSPlayback.js +5 -1
  83. package/dist/packages/core/src/components/Word/WordTableBlock.js +5 -2
  84. package/dist/packages/core/src/components/Word/WordTableButton.js +3 -0
  85. package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +5 -1
  86. package/dist/packages/core/src/components/Word/wordScoreStats.js +7 -2
  87. package/dist/packages/core/src/components/Word/wordTablePrint.js +15 -2
  88. package/dist/packages/core/src/components/WordTable.js +19 -5
  89. package/dist/packages/core/src/components/buttonTypes.d.ts +6 -0
  90. package/dist/packages/core/src/components/useCalendarMonth.js +15 -17
  91. package/dist/packages/core/src/components/useToastLifecycle.js +7 -2
  92. package/dist/packages/core/src/context/I18nProvider.js +12 -3
  93. package/dist/packages/core/src/hooks/useFontScale.js +13 -4
  94. package/dist/packages/core/src/hooks/useIcon.js +3 -0
  95. package/dist/packages/core/src/hooks/useLucideIcon.js +3 -0
  96. package/dist/packages/core/src/hooks/useMediaQuery.d.ts +2 -1
  97. package/dist/packages/core/src/hooks/useMediaQuery.js +37 -8
  98. package/dist/packages/core/src/hooks/usePagedList.js +1 -0
  99. package/dist/packages/core/src/hooks/useSoundCompassExam.js +13 -2
  100. package/dist/packages/core/src/hooks/useTrainingTypeLabels.js +1 -0
  101. package/dist/packages/core/src/hooks/useVisitHistory.js +1 -0
  102. package/dist/packages/core/src/hooks/useWordTrainingDataGeneration.js +18 -3
  103. package/dist/packages/core/src/i18n/locales/en.json +42 -4
  104. package/dist/packages/core/src/i18n/locales/ko.json +42 -4
  105. package/dist/packages/core/src/utils/date.js +26 -5
  106. package/dist/packages/core/src/utils/errorMessage.js +7 -1
  107. package/dist/packages/core/src/utils/speak.js +23 -3
  108. package/dist/packages/core/src/utils/textSort.js +12 -1
  109. package/dist/packages/core/src/utils/ttsConfig.js +10 -8
  110. package/dist/packages/core/src/version.d.ts +1 -1
  111. package/dist/packages/core/src/version.js +1 -1
  112. package/dist/utils.cjs.js +2 -2
  113. 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.135.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 ?? 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" }))] }));
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
  }
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useTranslation } from 'react-i18next';
3
4
  import './Breadcrumb.css';
@@ -432,6 +432,11 @@
432
432
  cursor: default;
433
433
  }
434
434
 
435
+ /* 툴팁만 보여 주는 칩 — 누를 수 있다고 말하지 않는다 */
436
+ .button.button--tooltip-only {
437
+ cursor: default;
438
+ }
439
+
435
440
 
436
441
 
437
442
 
@@ -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": isLink && disabledProp ? true : undefined, style: style, onClick: handleClickInternal, onMouseLeave: handleMouseLeave, onPointerEnter: (e) => {
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
- }, [indeterminate]);
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 => onChange?.(e.target.checked), disabled: disabled, "aria-label": !label && ariaLabel ? ariaLabel : undefined }), _jsx("span", { className: "checkbox__box" }), label && _jsx("span", { className: "checkbox__label body-3", children: label })] }));
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
- const isBash = /sh|bash/.test(lang.toLowerCase());
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
- onCompleteRef.current?.();
115
- return;
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 = {}) => (_jsx("img", { src: src, alt: typeof label === 'string' ? label : (label ? String(label) : ''), className: defaultImgClass, ...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
- const initial = selected !== undefined ? selected : ((categories && categories[0]?.key) ?? null);
21
- const [selectedKey, setSelectedKey] = useState(initial ?? null);
22
- // 외부 selected prop이 controlled로 전달되면 내부 상태 동기화하는 의도된 동기화다
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
- /* null 도 따른다 — 예전에는 null 을 걸러 버려, 부모가 selected={null} 로 선택을 풀어도
26
- 단추는 골라진 채로 남았다. */
27
- if (selected !== undefined) {
28
- setSelectedKey(selected);
29
- }
30
- }, [selected]);
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
- setSelectedKey(next);
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 = { open: '메뉴 열기', close: '메뉴 닫기' }, }) {
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 가 엉뚱한 서랍을 가리킨다 — 헤더마다 따로 짓는다.
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useTranslation } from 'react-i18next';
3
4
  import './PageTitle.css';
@@ -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, " / ", totalPages] }), _jsx(Button, { htmlType: "button", size: 32, type: "dot", shape: "rounded", icon: "chevron-right", ariaLabel: resolvedNext, disabled: page >= totalPages, onClick: handleNext })] }));
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: "panel__body--scroll", children: children })) : children] }));
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
  }
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  import { useParentBgTheme } from '../../hooks/useParentBgTheme';
3
4
  import './PassCardBg.css';
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useParentBgTheme } from '../../hooks/useParentBgTheme';
3
4
  import './PassCardBg.css';
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useParentBgTheme } from '../../hooks/useParentBgTheme';
3
4
  import { IHAB_OUTLINE_PATH } from './ihabOutlinePath';
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useParentBgTheme } from '../../hooks/useParentBgTheme';
3
4
  import { IHAB_OUTLINE_PATH } from './ihabOutlinePath';
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useParentBgTheme } from '../../hooks/useParentBgTheme';
3
4
  import './PassCardBg.css';
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useParentBgTheme } from '../../hooks/useParentBgTheme';
3
4
  import './PassCardBg.css';
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useParentBgTheme } from '../../hooks/useParentBgTheme';
3
4
  import './PassCardBg.css';
@@ -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: isSelected ? 'popup-list__row--selected' : col.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, "\uAC1C"] })] })] }, 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
+ 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))) })] }), _jsxs("span", { className: "load-table-popup__row-count caption-1", children: [table.items.length, "\uAC1C"] }), _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
+ : 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
  }