@ihabdevteam/core 0.131.0 → 0.133.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 (90) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/dist/components.cjs.js +178 -178
  3. package/dist/components.esm.css +2 -2
  4. package/dist/hooks.cjs.js +1 -1
  5. package/dist/i18n.cjs.js +1 -1
  6. package/dist/ihabdevteam-core.cjs.js +181 -181
  7. package/dist/ihabdevteam-core.d.ts +55 -17
  8. package/dist/packages/core/src/components/Accordion.js +4 -1
  9. package/dist/packages/core/src/components/Badge.d.ts +5 -0
  10. package/dist/packages/core/src/components/Badge.js +3 -3
  11. package/dist/packages/core/src/components/BarChart.d.ts +1 -1
  12. package/dist/packages/core/src/components/BarChart.js +25 -14
  13. package/dist/packages/core/src/components/BottomNav.js +3 -3
  14. package/dist/packages/core/src/components/BulkBar.js +1 -1
  15. package/dist/packages/core/src/components/Calendar.js +4 -0
  16. package/dist/packages/core/src/components/ChartSection.css +16 -0
  17. package/dist/packages/core/src/components/ChartSection.d.ts +8 -1
  18. package/dist/packages/core/src/components/ChartSection.js +2 -2
  19. package/dist/packages/core/src/components/ChoiceChips.js +1 -1
  20. package/dist/packages/core/src/components/CodeBox.js +1 -1
  21. package/dist/packages/core/src/components/DatePicker.js +16 -3
  22. package/dist/packages/core/src/components/Dropdown.js +13 -3
  23. package/dist/packages/core/src/components/FileUpload.js +8 -7
  24. package/dist/packages/core/src/components/FilterPanel.js +8 -5
  25. package/dist/packages/core/src/components/FilterSectionInfo.js +1 -1
  26. package/dist/packages/core/src/components/Header.js +1 -1
  27. package/dist/packages/core/src/components/HearingDeviceInfo.js +1 -1
  28. package/dist/packages/core/src/components/PageTitle.css +6 -0
  29. package/dist/packages/core/src/components/PageTitle.d.ts +9 -1
  30. package/dist/packages/core/src/components/PageTitle.js +3 -3
  31. package/dist/packages/core/src/components/Pagination.js +1 -1
  32. package/dist/packages/core/src/components/PanelRow.js +3 -3
  33. package/dist/packages/core/src/components/PaymentPlan.js +1 -1
  34. package/dist/packages/core/src/components/Popup/DeviceSideTable.js +2 -2
  35. package/dist/packages/core/src/components/Popup/LoadTablePopup.js +2 -2
  36. package/dist/packages/core/src/components/Popup/Popup.d.ts +8 -0
  37. package/dist/packages/core/src/components/Popup/Popup.js +17 -3
  38. package/dist/packages/core/src/components/Popup/SaveTablePopup.js +1 -1
  39. package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +1 -1
  40. package/dist/packages/core/src/components/Popup/WordDataUpdatePopup.js +3 -3
  41. package/dist/packages/core/src/components/RingChart.d.ts +3 -1
  42. package/dist/packages/core/src/components/RingChart.js +2 -2
  43. package/dist/packages/core/src/components/SegmentedControl.js +1 -1
  44. package/dist/packages/core/src/components/SideNav.js +27 -10
  45. package/dist/packages/core/src/components/Table.d.ts +17 -1
  46. package/dist/packages/core/src/components/Table.js +15 -9
  47. package/dist/packages/core/src/components/Tabs.js +7 -2
  48. package/dist/packages/core/src/components/TextField.js +34 -5
  49. package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +1 -1
  50. package/dist/packages/core/src/components/Tooltip/TextBoxTooltip.js +1 -1
  51. package/dist/packages/core/src/components/Tooltip/Tooltip.js +13 -5
  52. package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +1 -1
  53. package/dist/packages/core/src/components/Training/TrainingBlock.js +1 -1
  54. package/dist/packages/core/src/components/Training/TrainingBlockButton.js +1 -1
  55. package/dist/packages/core/src/components/Training/TrainingCardGroup.js +1 -1
  56. package/dist/packages/core/src/components/Training/TrainingPagePopup.js +51 -23
  57. package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +1 -1
  58. package/dist/packages/core/src/components/Training/TrainingPagePopupTitle.js +1 -1
  59. package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +26 -5
  60. package/dist/packages/core/src/components/Training/TrainingStepConfirm.js +1 -1
  61. package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +2 -2
  62. package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +1 -1
  63. package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +1 -1
  64. package/dist/packages/core/src/components/Training/TrainingStepTitle.js +1 -1
  65. package/dist/packages/core/src/components/Training/TrainingWordCard.js +11 -6
  66. package/dist/packages/core/src/components/Training/useAudioPreview.js +7 -3
  67. package/dist/packages/core/src/components/UpdateAlert.js +1 -1
  68. package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +2 -2
  69. package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +3 -3
  70. package/dist/packages/core/src/components/Word/WordTableBlock.js +2 -2
  71. package/dist/packages/core/src/components/Word/WordTableButton.js +1 -1
  72. package/dist/packages/core/src/components/WordTable.js +6 -6
  73. package/dist/packages/core/src/components/barChartTypes.d.ts +2 -0
  74. package/dist/packages/core/src/components/useComboboxSuggestions.d.ts +1 -0
  75. package/dist/packages/core/src/components/useComboboxSuggestions.js +12 -6
  76. package/dist/packages/core/src/components/useToastLifecycle.js +8 -1
  77. package/dist/packages/core/src/hooks/useAnchoredMenu.js +10 -9
  78. package/dist/packages/core/src/hooks/useDocumentTitle.d.ts +0 -11
  79. package/dist/packages/core/src/hooks/useDocumentTitle.js +16 -3
  80. package/dist/packages/core/src/hooks/useFocusTrap.js +6 -2
  81. package/dist/packages/core/src/utils/audioPeaks.js +13 -5
  82. package/dist/packages/core/src/utils/csv.js +3 -1
  83. package/dist/packages/core/src/utils/koreanSyllable.js +2 -1
  84. package/dist/packages/core/src/utils/textSort.d.ts +0 -10
  85. package/dist/packages/core/src/utils/textSort.js +7 -2
  86. package/dist/packages/core/src/utils/wordTrainingRuntime.js +28 -4
  87. package/dist/packages/core/src/version.d.ts +1 -1
  88. package/dist/packages/core/src/version.js +1 -1
  89. package/dist/utils.cjs.js +1 -1
  90. package/package.json +1 -1
@@ -230,6 +230,11 @@ export declare interface BadgeProps {
230
230
  * bar 는 주면 그 이름으로 읽힌다(안 주면 글자 그대로 읽힌다).
231
231
  */
232
232
  'aria-label'?: string;
233
+ /**
234
+ * 낭독기에서 숨긴다(dot·bar 모두). 개수를 곁의 단추 이름에 넣었을 때 쓴다 — 예: SideNav·BottomNav 는
235
+ * 단추 이름을 "숙제, 3개" 로 짓고 뱃지는 숨긴다. 주면 `aria-label` 보다 이긴다.
236
+ */
237
+ 'aria-hidden'?: boolean;
233
238
  label?: string;
234
239
  children?: ReactNode;
235
240
  /**
@@ -290,7 +295,7 @@ export declare type BadgeType = 'bar' | 'dot';
290
295
  * - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(일~토) 막대
291
296
  * 원시 렌더링은 내부 BarChartCanvas가 담당한다.
292
297
  */
293
- export declare function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, tightMax, avgMode, totalFormat, series, loading, onDetail, }: BarChartProps): JSX.Element;
298
+ export declare function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, tightMax, avgMode, totalFormat, series, loading, onDetail, inGrid, }: BarChartProps): JSX.Element;
294
299
 
295
300
  /** @public */
296
301
  export declare interface BarChartControl {
@@ -382,6 +387,8 @@ export declare interface BarChartProps {
382
387
  loading?: boolean;
383
388
  /** 지정 시 헤더 우측에 '상세보기' 버튼 표시 → 클릭 콜백 */
384
389
  onDetail?: () => void;
390
+ /** CSS 격자 칸 안에 세운다 — 아래 여백·최소 폭을 풀고 막대를 카드 바닥에 맞춘다. ChartSection `inGrid` 참고. */
391
+ inGrid?: boolean;
385
392
  }
386
393
 
387
394
  /** 시맨틱 사이즈 — 차트 캔버스 높이·막대 너비 프리셋 */
@@ -906,7 +913,7 @@ export declare interface ChartPoint {
906
913
  * 공유하도록 두 컴포넌트가 내부적으로 사용한다.
907
914
  * @public
908
915
  */
909
- export declare function ChartSection({ title, titleClassName, headRight, subRow, className, children }: ChartSectionProps): JSX.Element;
916
+ export declare function ChartSection({ title, titleClassName, headRight, subRow, className, inGrid, children }: ChartSectionProps): JSX.Element;
910
917
 
911
918
  /** @public */
912
919
  export declare interface ChartSectionProps {
@@ -920,6 +927,13 @@ export declare interface ChartSectionProps {
920
927
  subRow?: ReactNode;
921
928
  /** 섹션 루트에 추가할 클래스 */
922
929
  className?: string;
930
+ /**
931
+ * CSS 격자(한 줄에 여러 칸) 안에 세울 때 켠다 — `manager-section--grid`.
932
+ * 아래 여백(14px)·최소 폭(320px)을 풀고 카드를 세로 flex 로 만들어, 같은 줄 카드끼리 높이가 늘어나면
933
+ * 차트 몸통(막대·링·로딩)을 **카드 바닥**에 맞춘다(범례 줄 수가 달라도 막대 바닥이 한 선).
934
+ * 차트가 아닌 카드에 같은 꼴이 필요하면 `manager-section manager-section--grid` 클래스를 그대로 쓴다.
935
+ */
936
+ inGrid?: boolean;
923
937
  children?: ReactNode;
924
938
  }
925
939
 
@@ -1355,7 +1369,7 @@ export declare function copyText(text: string): Promise<boolean>;
1355
1369
  *
1356
1370
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
1357
1371
  */
1358
- export declare const CORE_VERSION = "0.131.0";
1372
+ export declare const CORE_VERSION = "0.133.0";
1359
1373
 
1360
1374
  /** @public */
1361
1375
  declare const coreDefault: Record<string, unknown>;
@@ -3246,7 +3260,7 @@ export declare type PageProps = {
3246
3260
  export declare function pageStartIndexes(pageLengths: readonly number[]): number[];
3247
3261
 
3248
3262
  /** @public */
3249
- export declare function PageTitle({ title, sub, rightAction, titleAction, badge, badgeVariant, documentTitle, onBack, backLabel }: PageTitleProps): JSX.Element;
3263
+ export declare function PageTitle({ title, sub, rightAction, titleAction, badge, badgeVariant, documentTitle, onBack, backLabel, wrap }: PageTitleProps): JSX.Element;
3250
3264
 
3251
3265
  declare type PageTitleProps = {
3252
3266
  title: ReactNode;
@@ -3277,6 +3291,14 @@ declare type PageTitleProps = {
3277
3291
  * title 이 문자열이면 이 값은 필요 없다.
3278
3292
  */
3279
3293
  documentTitle?: string;
3294
+ /**
3295
+ * 오른쪽(`rightAction`)이 넓어 제목 칸을 짓누를 때 **줄을 접어** 오른쪽을 아래로 내린다. 기본 false(한 줄 유지).
3296
+ *
3297
+ * 한 줄로 두면 제목 칸이 `min-width: 0` 까지 줄어, 탭 일곱 같은 넓은 오른쪽이 제목을 한 글자씩 세로로
3298
+ * 밀어낸다(말귀 고객 상세 1135px 실측). 켜면 제목 칸에 320px(좁으면 제 폭) 바닥을 주고, 그 바닥이
3299
+ * 안 들어가면 오른쪽이 다음 줄로 간다.
3300
+ */
3301
+ wrap?: boolean;
3280
3302
  };
3281
3303
 
3282
3304
  /**
@@ -4125,6 +4147,14 @@ export declare interface PopupProps {
4125
4147
  * 더 넓거나 무제한이 필요하면 className으로 `.popup-panel` max-width를 직접 override.
4126
4148
  */
4127
4149
  size?: 'small' | 'medium' | 'large';
4150
+ /**
4151
+ * 패널 높이를 못 박는다(숫자는 px, 문자열은 CSS 길이 그대로). 미지정 시 내용만큼(기본).
4152
+ *
4153
+ * 탭을 옮길 때마다 내용 높이가 달라 창이 위아래로 뛰는 팝업에 쓴다. **화면보다 크면 화면에
4154
+ * 맞춘다** — 패널의 `max-height`(데스크톱 `100% - 60px`, 좁은 화면은 하단 내비 몫까지 뺀 값)가
4155
+ * 그대로 걸리므로 창이 화면 밖으로 나가지 않는다. 넘치는 내용은 본문이 스스로 굴린다.
4156
+ */
4157
+ height?: number | string;
4128
4158
  /**
4129
4159
  * 이 값이 바뀌면 본문(.popup-content) 스크롤을 맨 위로 되돌린다.
4130
4160
  * 탭/모드 전환처럼 내부 콘텐츠가 통째로 바뀌는 팝업에서, 스크롤을 내린 채
@@ -4904,7 +4934,7 @@ export declare function reviewTableName(original: string, today: Date): string;
4904
4934
  * 미지정 시에는 헤더 없는 순수 링만 렌더된다(크기/색상 데모 등).
4905
4935
  * @public
4906
4936
  */
4907
- export declare function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, ...ringProps }: RingChartProps): JSX.Element;
4937
+ export declare function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid, ...ringProps }: RingChartProps): JSX.Element;
4908
4938
 
4909
4939
  /** @public */
4910
4940
  export declare interface RingChartProps {
@@ -4955,6 +4985,8 @@ export declare interface RingChartProps {
4955
4985
  loading?: boolean;
4956
4986
  /** 섹션 루트에 추가할 클래스 (titleKey 지정 시에만 사용) */
4957
4987
  className?: string;
4988
+ /** CSS 격자 칸 안에 세운다 — 아래 여백·최소 폭을 풀고 링을 카드 바닥에 맞춘다. ChartSection `inGrid` 참고(titleKey 지정 시에만 사용) */
4989
+ inGrid?: boolean;
4958
4990
  }
4959
4991
 
4960
4992
  /** @public */
@@ -6017,7 +6049,7 @@ export declare type TabItem = {
6017
6049
  /** @public */
6018
6050
  export declare function Table<T extends {
6019
6051
  id?: string | number;
6020
- }>({ fields, headerGroups, items, renderRow, onRowClick, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, resizable, resizeMode, resizeStorageKey, size, title, icon, count, collapsed, onToggleCollapse, headerActions, empty, loading, dragOver, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }: TableProps<T>): JSX.Element;
6052
+ }>({ fields, headerGroups, items, renderRow, onRowClick, isRowClickable, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, mobileGridTemplateColumns, resizable, resizeMode, resizeStorageKey, size, title, icon, count, collapsed, onToggleCollapse, headerActions, empty, loading, dragOver, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }: TableProps<T>): JSX.Element;
6021
6053
 
6022
6054
  /**
6023
6055
  * **다음에 쓸 표를 고른다.**
@@ -6139,12 +6171,28 @@ export declare type TableProps<T extends {
6139
6171
  style?: CSSProperties;
6140
6172
  }>;
6141
6173
  onRowClick?: (item: T) => void;
6174
+ /**
6175
+ * 줄마다 누를 수 있는지. false 인 줄은 `onRowClick` 이 안 불리고, 버튼으로 읽히지도(role·Tab 정지점) 않으며,
6176
+ * 손가락 커서·hover 강조도 없다. `onRowClick` 이 있을 때만 뜻이 있다. 미지정이면 모든 줄이 눌린다(기본).
6177
+ *
6178
+ * 예: 방문 기록 표에서 '안 옴' 줄은 열 것이 없다 — CSS 로 커서만 되돌리면 눌리기는 그대로이고 낭독기는 여전히 버튼이라 한다.
6179
+ */
6180
+ isRowClickable?: (item: T) => boolean;
6142
6181
  onRowMouseEnter?: (item: T, event: MouseEvent_2<HTMLDivElement>) => void;
6143
6182
  onRowMouseLeave?: (item: T, event: MouseEvent_2<HTMLDivElement>) => void;
6144
6183
  onRowDragStart?: (item: T, event: DragEvent_2<HTMLDivElement>) => void;
6145
6184
  onRowDragEnd?: (item: T, event: DragEvent_2<HTMLDivElement>) => void;
6146
6185
  draggingId?: string | number | null;
6147
6186
  gridTemplateColumns?: string;
6187
+ /**
6188
+ * 좁은 화면(767px 이하)에서 쓸 열 틀. 주면 그 폭에서는 이 틀을 쓰고 **끌어 둔 폭(저장값)은 무시**한다 —
6189
+ * 폭 조절 손잡이와 '칸 너비 초기화' 단추도 숨겨, 좁은 화면에서 끈 값이 넓은 화면의 저장값을 덮지 않게 한다.
6190
+ * 넓은 화면으로 돌아오면 저장값이 그대로 돌아온다. 미지정이면 좁은 화면도 넓은 화면과 같다(기본).
6191
+ *
6192
+ * 열 틀은 인라인 CSS 변수로 들어가므로 앱 CSS 로는 `!important` 없이 못 이긴다 — 그래서 prop 이다.
6193
+ * 숨길 칸은 `renderRow` 의 `className`·`fields[].className` 으로 앱이 가린다.
6194
+ */
6195
+ mobileGridTemplateColumns?: string;
6148
6196
  /** true면 헤더 경계선을 드래그해 컬럼 폭 조절 가능 */
6149
6197
  resizable?: boolean;
6150
6198
  /**
@@ -7760,6 +7808,7 @@ export declare function useComboboxSuggestions({ value, onChange, suggestions, o
7760
7808
  filtered: string[];
7761
7809
  handleFocus: () => void;
7762
7810
  handleBlur: () => void;
7811
+ handleInput: () => void;
7763
7812
  handleKeyDown: (e: KeyboardEvent_2<HTMLInputElement | HTMLTextAreaElement>) => void;
7764
7813
  handleSelect: (item: string) => void;
7765
7814
  };
@@ -7834,17 +7883,6 @@ export declare function useDeviceManagement(): {
7834
7883
  handleSelectFromList: (form: DeviceForm) => void;
7835
7884
  };
7836
7885
 
7837
- /**
7838
- * 현재 화면의 제목을 `document.title`에 반영한다 — `'<제목> · <앱 이름>'`.
7839
- *
7840
- * SPA는 라우트를 옮겨도 문서 제목이 그대로라, 브라우저 탭·히스토리·북마크로 위치를
7841
- * 구분할 수 없고 화면낭독기도 이동을 인지하지 못한다(WCAG 2.4.2).
7842
- * 언마운트하면 이전 제목으로 되돌린다.
7843
- *
7844
- * @param title 페이지 제목. 비어 있거나 문자열이 아니면 아무것도 하지 않는다.
7845
- *
7846
- * @public
7847
- */
7848
7886
  export declare function useDocumentTitle(title?: string | null): void;
7849
7887
 
7850
7888
  /**
@@ -6,7 +6,10 @@ import Icon from './Icon';
6
6
  import './Accordion.css';
7
7
  function Item({ title, body, open = false, onToggle, avatar, titleClassName = 'body-2', bodyClassName = 'body-2', className = '', }) {
8
8
  const { t } = useTranslation();
9
- return (_jsxs("div", { className: `accordion-item ${open ? 'accordion-item--open' : ''} ${className}`.trim(), children: [_jsxs("div", { className: "accordion-item__header", role: "button", tabIndex: 0, "aria-expanded": open, "data-accordion-header": true, onClick: onToggle, onKeyDown: e => {
9
+ return (
10
+ /* 무리가 role="list" 라 항목은 listitem 이어야 한다 — 없으면 낭독기가 "목록, 항목 0개" 로 읽고
11
+ 목록 안 차례("3개 중 2번째")도 못 준다. */
12
+ _jsxs("div", { className: `accordion-item ${open ? 'accordion-item--open' : ''} ${className}`.trim(), role: "listitem", children: [_jsxs("div", { className: "accordion-item__header", role: "button", tabIndex: 0, "aria-expanded": open, "data-accordion-header": true, onClick: onToggle, onKeyDown: e => {
10
13
  if (e.key === 'Enter' || e.key === ' ') {
11
14
  // div에 role=button을 씌운 것이라 Space의 기본 동작(페이지 스크롤)이 살아 있다.
12
15
  // 막지 않으면 키보드로 항목을 열 때마다 화면이 한 페이지씩 밀려 내려간다.
@@ -32,6 +32,11 @@ export interface BadgeProps {
32
32
  * bar 는 주면 그 이름으로 읽힌다(안 주면 글자 그대로 읽힌다).
33
33
  */
34
34
  'aria-label'?: string;
35
+ /**
36
+ * 낭독기에서 숨긴다(dot·bar 모두). 개수를 곁의 단추 이름에 넣었을 때 쓴다 — 예: SideNav·BottomNav 는
37
+ * 단추 이름을 "숙제, 3개" 로 짓고 뱃지는 숨긴다. 주면 `aria-label` 보다 이긴다.
38
+ */
39
+ 'aria-hidden'?: boolean;
35
40
  label?: string;
36
41
  children?: ReactNode;
37
42
  /**
@@ -18,7 +18,7 @@ function MinusIcon() {
18
18
  /** @public */
19
19
  export default function Badge(props) {
20
20
  const { t } = useTranslation();
21
- const { type = 'bar', size, className = '', 'aria-label': ariaLabel, } = props;
21
+ const { type = 'bar', size, className = '', 'aria-label': ariaLabel, 'aria-hidden': ariaHidden, } = props;
22
22
  // ── dot (원형) ──────────────────────────────────────────────────────────
23
23
  if (type === 'dot') {
24
24
  const { icon, number, position = 'top-right', onClick, animate = false } = props;
@@ -49,7 +49,7 @@ export default function Badge(props) {
49
49
  내게** 한다 — 그래야 이벤트의 target·currentTarget 도 실제와 같다. */
50
50
  e.preventDefault();
51
51
  e.currentTarget.click();
52
- } : undefined, "aria-label": ariaLabel ?? defaultLabel, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, children: [number !== undefined && number, number === undefined && dotIcon === 'check' && _jsx(CheckIcon, {}), number === undefined && dotIcon === 'remove' && _jsx(RemoveIcon, {}), number === undefined && dotIcon === 'minus' && _jsx(MinusIcon, {})] }));
52
+ } : undefined, "aria-label": ariaHidden ? undefined : ariaLabel ?? defaultLabel, "aria-hidden": ariaHidden || undefined, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, children: [number !== undefined && number, number === undefined && dotIcon === 'check' && _jsx(CheckIcon, {}), number === undefined && dotIcon === 'remove' && _jsx(RemoveIcon, {}), number === undefined && dotIcon === 'minus' && _jsx(MinusIcon, {})] }));
53
53
  }
54
54
  // ── bar (텍스트) ────────────────────────────────────────────────────────
55
55
  const { label, children, variant = 'default', fill = 'filled', minWidth, iconLeft, iconRight, color } = props;
@@ -72,5 +72,5 @@ export default function Badge(props) {
72
72
  return (
73
73
  // aria-label 을 받아 놓고 bar 에서는 버렸다 — dot 과 같이 건넨다. 이름만 두면 역할 없는
74
74
  // span 이라 낭독기가 무시하므로 이름이 있을 때만 role="img" 를 붙인다(안 주면 예전 그대로).
75
- _jsxs("span", { className: cls, style: style, "aria-label": ariaLabel, role: ariaLabel ? 'img' : undefined, children: [iconLeft && _jsx(Icon, { icon: iconLeft, size: iconSize, className: "badge__icon" }), label ?? children, iconRight && _jsx(Icon, { icon: iconRight, size: iconSize, className: "badge__icon" })] }));
75
+ _jsxs("span", { className: cls, style: style, "aria-label": ariaHidden ? undefined : ariaLabel, role: ariaLabel && !ariaHidden ? 'img' : undefined, "aria-hidden": ariaHidden || undefined, children: [iconLeft && _jsx(Icon, { icon: iconLeft, size: iconSize, className: "badge__icon" }), label ?? children, iconRight && _jsx(Icon, { icon: iconRight, size: iconSize, className: "badge__icon" })] }));
76
76
  }
@@ -13,4 +13,4 @@ import type { BarChartProps } from './barChartTypes';
13
13
  * - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(일~토) 막대
14
14
  * 원시 렌더링은 내부 BarChartCanvas가 담당한다.
15
15
  */
16
- export default function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, tightMax, avgMode, totalFormat, series, loading, onDetail, }: BarChartProps): import("react").JSX.Element;
16
+ export default function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, tightMax, avgMode, totalFormat, series, loading, onDetail, inGrid, }: BarChartProps): import("react").JSX.Element;
@@ -12,13 +12,17 @@ import ChartSection, { ChartTotal, ChartLegend } from './ChartSection';
12
12
  import { firstMondayOfMonth, KO_ORD } from './barChartData';
13
13
  import { useBarChartControl } from './useBarChartControl';
14
14
  // ─── 막대 렌더링 원시 컴포넌트 ───────────────────────────────────
15
+ /** 막대 값 — 수가 아니면(undefined·NaN·Infinity) 0 */
16
+ const 수 = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
15
17
  function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, height = 140, maxOverride, tightMax, seriesColors, seriesLabels, rateLabels }) {
16
18
  // colorB 미지정 시 단일 계열(A) 모드
17
19
  const single = !colorB;
18
20
  const stacked = !!(seriesColors && seriesColors.length > 0);
21
+ /* 값이 비었거나(undefined) 수가 아니면 0 으로 친다. 두 계열인데 한 항목의 `b` 가 비면 Math.max 가 NaN 이 되고
22
+ `NaN || 10` 으로 y축이 데이터와 상관없이 10 에 박혀 막대가 판 위로 넘쳤다(ihab-core-96 이 짚음). */
19
23
  const maxVal = useMemo(() => stacked
20
- ? Math.max(0, ...data.filter(d => d.a !== -1).map(d => (d.parts ?? []).reduce((s, v) => s + Math.max(0, v), 0)))
21
- : Math.max(0, ...data.filter(d => d.a !== -1).flatMap(d => single ? [d.a] : [d.a, d.b])), [data, single, stacked]);
24
+ ? Math.max(0, ...data.filter(d => d.a !== -1).map(d => (d.parts ?? []).reduce((s, v) => s + Math.max(0, 수(v)), 0)))
25
+ : Math.max(0, ...data.filter(d => d.a !== -1).flatMap(d => single ? [수(d.a)] : [수(d.a), 수(d.b)])), [data, single, stacked]);
22
26
  const padded = useMemo(() => maxOverride ?? (tightMax ? (maxVal > 0 ? maxVal : 10) : (Math.ceil(maxVal / 10) * 10 || 10)), [maxVal, maxOverride, tightMax]);
23
27
  const h = height;
24
28
  // 막대 너비 — 여백과 무관하게 막대 '개수'(컬럼 × 계열)가 늘수록 점진 축소(최대 barWidth)
@@ -38,9 +42,11 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, h
38
42
  const handleBarLeave = useCallback(() => setTooltip(null), []);
39
43
  return (_jsxs("div", { className: "manager-bar-chart-wrap", children: [_jsx("div", { className: "manager-bar-chart-inner", children: _jsx("div", { className: "manager-bar-chart-scroll", children: _jsxs("div", { className: "manager-bar-chart-area", children: [_jsxs("div", { className: "manager-bar-chart-plot", style: { height: h }, children: [ticks.slice(0, -1).map((t, i) => (_jsx("div", { className: "manager-bar-chart-gridline", style: { top: i * (h / (ticks.length - 1)) }, children: _jsx("span", { className: "manager-bar-chart-gridline__label caption-1", children: t }) }, i))), data.map((d, i) => {
40
44
  const isFuture = d.a === -1;
41
- const ha = isFuture ? 0 : (d.a / padded) * h;
42
- const hb = isFuture ? 0 : (d.b / padded) * h;
43
- const parts = d.parts ?? [];
45
+ /* 판 높이를 넘지 않는다 — maxOverride(진행률 100)보다 큰 값이 와도 막대가 눈금 위로 삐져나가지 않게 */
46
+ const 높이 = (v) => Math.min(h, Math.max(0, (수(v) / padded) * h));
47
+ const ha = isFuture ? 0 : 높이(d.a);
48
+ const hb = isFuture ? 0 : 높이(d.b);
49
+ const parts = (d.parts ?? []).map(수);
44
50
  return (_jsx("div", { className: "manager-bar-chart-col", style: { cursor: isFuture ? 'default' : 'pointer' }, onMouseEnter: isFuture ? undefined : (e) => {
45
51
  const r = e.currentTarget.getBoundingClientRect();
46
52
  /* 한 줄에 `A: 3 / B: 5 / …` 로 늘어놓던 것을 줄마다 하나로 — 유형이 넷·다섯이면
@@ -53,25 +59,30 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, h
53
59
  items = 있는.length ? 있는 : 전부;
54
60
  }
55
61
  else if (rateLabels && !single) {
56
- items = [{ text: `${labelA} ${d.a}%`, color: colorA }, { text: `${labelB} ${d.b}%`, color: colorB }];
62
+ items = [{ text: `${labelA} ${수(d.a)}%`, color: colorA }, { text: `${labelB} ${수(d.b)}%`, color: colorB }];
57
63
  }
58
64
  else if (rateLabels && d.rawB !== undefined) {
59
65
  items = [{ text: `${rateLabels.total} ${d.rawB}` }, { text: `${rateLabels.done} ${d.rawA ?? 0}`, color: colorA }];
60
66
  }
61
67
  else if (single) {
62
- items = [{ text: `${labelA}: ${d.a}`, color: colorA }];
68
+ items = [{ text: `${labelA}: ${수(d.a)}`, color: colorA }];
63
69
  }
64
70
  else {
65
- items = [{ text: `${labelA}: ${d.a}`, color: colorA }, { text: `${labelB}: ${d.b}`, color: colorB }];
71
+ items = [{ text: `${labelA}: ${수(d.a)}`, color: colorA }, { text: `${labelB}: ${수(d.b)}`, color: colorB }];
66
72
  }
67
73
  setTooltip({ rect: { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height }, title, items });
68
74
  }, onMouseLeave: handleBarLeave, children: !isFuture && (stacked ? (
69
75
  // 계열별 stacked — 아래부터 위로 쌓고, 맨 위 segment만 상단 라운드
70
76
  _jsx("div", { className: "manager-bar-chart-stack", style: 막대굵기, children: (() => {
71
77
  const topK = parts.reduce((acc, v, k) => (v > 0 ? k : acc), -1); // 맨 위(최상단) 세그먼트
72
- return parts.map((v, k) => (v > 0
73
- ? _jsx("div", { className: "manager-bar-chart-stack-segment", style: { height: (v / padded) * h, background: (seriesColors ?? [])[k], borderRadius: k === topK ? '6px 6px 0 0' : undefined } }, k)
74
- : null));
78
+ let 남은 = h; // 쌓은 합도 판 높이를 넘지 않게 위 조각부터 깎는다
79
+ return parts.map((v, k) => {
80
+ if (v <= 0)
81
+ return null;
82
+ const 조각 = Math.min(남은, (v / padded) * h);
83
+ 남은 -= 조각;
84
+ return _jsx("div", { className: "manager-bar-chart-stack-segment", style: { height: 조각, background: (seriesColors ?? [])[k], borderRadius: k === topK ? '6px 6px 0 0' : undefined } }, k);
85
+ });
75
86
  })() })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "manager-bar-chart-bar", style: { ...막대굵기, height: ha, background: colorA } }), !single && _jsx("div", { className: "manager-bar-chart-bar", style: { ...막대굵기, height: hb, background: colorB } })] }))) }, i));
76
87
  })] }), _jsx("div", { className: "manager-bar-chart-x-axis", children: data.map((d, i) => {
77
88
  const isFuture = d.a === -1; // 아직 도래하지 않은 날 — 글씨 흐리게
@@ -86,7 +97,7 @@ export function BarChartControls({ control }) {
86
97
  주 보기에서 낭독기·툴팁이 실제 동작과 다른 말을 한다(말귀 검수 실측). */
87
98
  const prevLabel = period === 'week' ? t('calendar.prevWeek', '이전 주') : t('calendar.prevMonth', '이전 달');
88
99
  const nextLabel = period === 'week' ? t('calendar.nextWeek', '다음 주') : t('calendar.nextMonth', '다음 달');
89
- return (_jsxs("div", { className: "manager-section-controls", children: [_jsx(Button, { size: 32, type: "dot", icon: "chevron-left", ariaLabel: prevLabel, showTooltipInfo: prevLabel, disabled: activeIdx === activeOptions.length - 1, onClick: handlePrev }), _jsx(Dropdown, { value: range, onChange: setRange, options: activeOptions, size: 40, className: "month-dropdown", ariaLabel: t('calendar.rangeSelect', '기간 선택') }), _jsx(Button, { size: 32, type: "dot", icon: "chevron-right", ariaLabel: nextLabel, showTooltipInfo: nextLabel, disabled: activeIdx <= 0, onClick: handleNext }), _jsx(SegmentedControl, { ariaLabel: t('calendar.periodSelect', '기간 단위'), options: periodOptions, value: period, onChange: (k) => setPeriod(k), size: 32 })] }));
100
+ return (_jsxs("div", { className: "manager-section-controls", children: [_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "chevron-left", ariaLabel: prevLabel, showTooltipInfo: prevLabel, disabled: activeIdx === activeOptions.length - 1, onClick: handlePrev }), _jsx(Dropdown, { value: range, onChange: setRange, options: activeOptions, size: 40, className: "month-dropdown", ariaLabel: t('calendar.rangeSelect', '기간 선택') }), _jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "chevron-right", ariaLabel: nextLabel, showTooltipInfo: nextLabel, disabled: activeIdx <= 0, onClick: handleNext }), _jsx(SegmentedControl, { ariaLabel: t('calendar.periodSelect', '기간 단위'), options: periodOptions, value: period, onChange: (k) => setPeriod(k), size: 32 })] }));
90
101
  }
91
102
  /**
92
103
  * 한 주(월요일부터 7일)의 날마다 — 그 날의 자료와 미래 여부. 달을 넘는 날은 그 달
@@ -167,7 +178,7 @@ const BAR_SIZE_PRESET = {
167
178
  * - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(일~토) 막대
168
179
  * 원시 렌더링은 내부 BarChartCanvas가 담당한다.
169
180
  */
170
- export default function BarChart({ size = 'medium', barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal = false, totalUnitKey, totalLabelKey, control: controlProp, hideControls = false, rate = false, tightMax = false, avgMode = false, totalFormat, series, loading = false, onDetail, }) {
181
+ export default function BarChart({ size = 'medium', barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal = false, totalUnitKey, totalLabelKey, control: controlProp, hideControls = false, rate = false, tightMax = false, avgMode = false, totalFormat, series, loading = false, onDetail, inGrid = false, }) {
171
182
  const { t, i18n } = useTranslation();
172
183
  const sizePreset = BAR_SIZE_PRESET[size];
173
184
  // 외부 control 미지정 시 내부 컨트롤 사용 (단독 차트 하위 호환)
@@ -228,7 +239,7 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
228
239
  // size='large' — 카드가 가로 폭을 거의 100% 차지(1열)
229
240
  const isLarge = size === 'large';
230
241
  const sectionClassName = isSmall ? 'manager-section--compact' : isLarge ? 'manager-section--full' : undefined;
231
- return (_jsx(ChartSection, { className: sectionClassName, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: isSmall ? undefined : _jsxs(_Fragment, { children: [!hideControls && _jsx(BarChartControls, { control: control }), onDetail && (_jsx("span", { className: "manager-bar-chart-detail-btn", children: _jsx(Button, { size: 32, type: "dot", icon: "list", ariaLabel: t('trainingStatus.detail', '상세보기'), onClick: onDetail }) }))] }), subRow: _jsxs(_Fragment, { children: [showTotal ? ((rate || avgMode || (!!totalLabelKey && !!totalFormat)) ? (
242
+ return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: isSmall ? undefined : _jsxs(_Fragment, { children: [!hideControls && _jsx(BarChartControls, { control: control }), onDetail && (_jsx("span", { className: "manager-bar-chart-detail-btn", children: _jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "list", ariaLabel: t('trainingStatus.detail', '상세보기'), onClick: onDetail }) }))] }), subRow: _jsxs(_Fragment, { children: [showTotal ? ((rate || avgMode || (!!totalLabelKey && !!totalFormat)) ? (
232
243
  // 진행률/평균 모드 또는 totalLabelKey+totalFormat 조합: 라벨(윗줄) + 값(아랫줄). size='small'은 라벨 생략
233
244
  _jsx(ChartTotal, { label: isSmall ? undefined : `${t(control.period === 'week' ? 'manager.chart.weeklyPrefix' : 'manager.chart.monthlyPrefix')} ${t(totalLabelKey ?? labelAKey)}`, children: totalFormat ? (
234
245
  // 숫자는 h0, 단위(시간/분/초 등)는 h5로 분리
@@ -97,8 +97,8 @@ export default function BottomNav({ items = [], activeKey, onChange, appMode, on
97
97
  }], [hiddenItems, moreItems, activeKey, onChange, handleCloseMore]);
98
98
  // "…" 단추 배지 — 메뉴 안에서만 보이는 개수(넘친 항목 + 유틸)를 닫힌 채로도 알린다.
99
99
  const moreBadge = [...hiddenItems, ...moreItems].reduce((sum, it) => sum + badgeOf(it.badgeCount), 0);
100
- return (_jsxs("nav", { className: "bottom-nav", "aria-label": t('nav.bottomNav', '하단 네비게이션'), children: [onAppModeChange && (_jsxs(_Fragment, { children: [_jsx("div", { className: "bottom-nav__left", children: _jsx(Button, { size: 52, type: "dot", icon: modeIcon, ariaLabel: modeLabel, className: "bottom-nav-item", onClick: handleModeChange }) }), _jsx("div", { className: "bottom-nav__divider", "aria-hidden": "true" })] })), _jsx("div", { className: "bottom-nav__items", ref: itemsRef, children: visibleItems.map((it) => {
100
+ return (_jsxs("nav", { className: "bottom-nav", "aria-label": t('nav.bottomNav', '하단 네비게이션'), children: [onAppModeChange && (_jsxs(_Fragment, { children: [_jsx("div", { className: "bottom-nav__left", children: _jsx(Button, { htmlType: "button", size: 52, type: "dot", icon: modeIcon, ariaLabel: modeLabel, className: "bottom-nav-item", onClick: handleModeChange }) }), _jsx("div", { className: "bottom-nav__divider", "aria-hidden": "true" })] })), _jsx("div", { className: "bottom-nav__items", ref: itemsRef, children: visibleItems.map((it) => {
101
101
  const count = badgeOf(it.badgeCount);
102
- return (_jsxs("span", { className: "bottom-nav__slot", children: [_jsx(Button, { size: 52, type: "dot", icon: it.icon, ariaLabel: nameWithCount(it.label, count), selected: it.key === activeKey, disabled: it.state === 'disabled', className: "bottom-nav-item", onClick: () => it.key && onChange?.(it.key) }), count > 0 && (_jsx("span", { "aria-hidden": "true", children: _jsx(Badge, { type: "dot", position: "top-right", number: Math.min(count, 99), size: 24 }) }))] }, it.key));
103
- }) }), showMore && (_jsxs(_Fragment, { children: [_jsx("div", { className: "bottom-nav__divider", "aria-hidden": "true" }), _jsx("div", { className: "bottom-nav__right", children: _jsxs("div", { ref: moreRef, className: "bottom-nav__slot", children: [_jsx(Button, { size: 52, type: "dot", icon: activeMore?.icon ?? 'ellipsis', ariaLabel: nameWithCount(activeMore?.label ?? t('nav.more', '더보기'), moreBadge), selected: !!activeMore, className: "bottom-nav-item", onClick: handleToggleMore, "aria-expanded": moreOpen, "aria-controls": moreOpen ? moreMenuId : undefined }), moreBadge > 0 && (_jsx("span", { "aria-hidden": "true", children: _jsx(Badge, { type: "dot", position: "top-right", number: Math.min(moreBadge, 99), size: 24 }) })), moreOpen && createPortal(_jsx("div", { id: moreMenuId, role: "group", "aria-label": t('nav.moreMenu', '더보기 메뉴'), className: "tooltip-content", style: menuStyle, ref: menuRef, children: _jsx(MenuTooltip, { columns: moreColumns, onClose: handleCloseMore }) }), document.body)] }) })] }))] }));
102
+ return (_jsxs("span", { className: "bottom-nav__slot", children: [_jsx(Button, { htmlType: "button", size: 52, type: "dot", icon: it.icon, ariaLabel: nameWithCount(it.label, count), selected: it.key === activeKey, disabled: it.state === 'disabled', className: "bottom-nav-item", onClick: () => it.key && onChange?.(it.key) }), count > 0 && (_jsx("span", { "aria-hidden": "true", children: _jsx(Badge, { type: "dot", position: "top-right", number: Math.min(count, 99), size: 24 }) }))] }, it.key));
103
+ }) }), showMore && (_jsxs(_Fragment, { children: [_jsx("div", { className: "bottom-nav__divider", "aria-hidden": "true" }), _jsx("div", { className: "bottom-nav__right", children: _jsxs("div", { ref: moreRef, className: "bottom-nav__slot", children: [_jsx(Button, { htmlType: "button", size: 52, type: "dot", icon: activeMore?.icon ?? 'ellipsis', ariaLabel: nameWithCount(activeMore?.label ?? t('nav.more', '더보기'), moreBadge), selected: !!activeMore, className: "bottom-nav-item", onClick: handleToggleMore, "aria-expanded": moreOpen, "aria-controls": moreOpen ? moreMenuId : undefined }), moreBadge > 0 && (_jsx("span", { "aria-hidden": "true", children: _jsx(Badge, { type: "dot", position: "top-right", number: Math.min(moreBadge, 99), size: 24 }) })), moreOpen && createPortal(_jsx("div", { id: moreMenuId, role: "group", "aria-label": t('nav.moreMenu', '더보기 메뉴'), className: "tooltip-content", style: menuStyle, ref: menuRef, children: _jsx(MenuTooltip, { columns: moreColumns, onClose: handleCloseMore }) }), document.body)] }) })] }))] }));
104
104
  }
@@ -43,5 +43,5 @@ export default function BulkBar({ count, actions, onClear, clearLabel: clearLabe
43
43
  단추가 Tab 순서와 낭독기에 남아, 보이지 않는 '선택 해제'·'삭제' 에 포커스가 갔다.
44
44
  inert 로 포커스·낭독기에서 함께 뺀다(보이는 모양은 CSS 그대로). aria-hidden 은
45
45
  inert 를 모르는 오래된 브라우저를 위한 덧댐이다. */
46
- inert: !show || undefined, "aria-hidden": show ? undefined : true, children: [_jsx("span", { ref: contentRef, className: "bulk-bar__count body-2", children: content ?? _jsx("b", { children: t('common.selectedCount', { count }) }) }), max !== undefined && (_jsx(Badge, { size: "small", fill: "soft", variant: "main", label: `${count} / ${max}` })), _jsxs("div", { className: `bulk-bar__actions${stacked ? ' bulk-bar__actions--stacked' : ''}`, children: [_jsx(Button, { size: 40, label: clearLabel, onClick: onClear }), actions.map((a, i) => (_jsx(Button, { size: 40, variant: a.variant ?? 'default', icon: a.icon, label: a.label, onClick: a.onClick }, i)))] })] }));
46
+ inert: !show || undefined, "aria-hidden": show ? undefined : true, children: [_jsx("span", { ref: contentRef, className: "bulk-bar__count body-2", children: content ?? _jsx("b", { children: t('common.selectedCount', { count }) }) }), max !== undefined && (_jsx(Badge, { size: "small", fill: "soft", variant: "main", label: `${count} / ${max}` })), _jsxs("div", { className: `bulk-bar__actions${stacked ? ' bulk-bar__actions--stacked' : ''}`, children: [_jsx(Button, { htmlType: "button", size: 40, label: clearLabel, onClick: onClear }), actions.map((a, i) => (_jsx(Button, { htmlType: "button", size: 40, variant: a.variant ?? 'default', icon: a.icon, label: a.label, onClick: a.onClick }, i)))] })] }));
47
47
  }
@@ -62,6 +62,10 @@ export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDay
62
62
  return (_jsxs("div", { className: `calendar-cell${isToday(day) ? ' today' : ''}${isDragOver ? ' drag-over' : ''}${onDayClick ? ' calendar-cell--clickable' : ''}`, role: onDayClick ? 'button' : undefined, tabIndex: onDayClick ? 0 : undefined, "aria-label": onDayClick ? t('calendar.addOnDate', { date: dateKey }) : undefined, onClick: onDayClick ? () => onDayClick(dateKey) : undefined,
63
63
  /* `<div role="button">` 은 브라우저가 Enter·Space 를 눌림으로 바꿔 주지 않는다 */
64
64
  onKeyDown: onDayClick ? (e) => {
65
+ /* 칸 안의 방문 표시에서 누른 Enter 가 올라온 것이면 넘긴다 — 마우스 click 은 표시가
66
+ stopPropagation 하지만 keydown 은 막지 않아, 방문 상세와 '그날 새 방문' 이 함께 열렸다. */
67
+ if (e.target !== e.currentTarget)
68
+ return;
65
69
  if (e.key !== 'Enter' && e.key !== ' ')
66
70
  return;
67
71
  e.preventDefault();
@@ -189,3 +189,19 @@
189
189
  min-width: 0;
190
190
  }
191
191
  }
192
+
193
+ /* inGrid — CSS 격자 칸 안의 카드. 격자가 간격(gap)과 폭을 정하므로 아래 여백·최소 폭을 푼다.
194
+ compact 의 min-width(160px)보다 뒤에 둬야 같은 특이도로 이긴다 — **파일 끝**에 둔다.
195
+ 세로 flex 라서, 같은 줄 카드끼리 늘어난 높이를 차트 몸통 위에 몰아 막대·링이 바닥에 선다
196
+ (말귀 훈련 현황의 네 칸이 범례 줄 수가 달라 막대 바닥이 들쭉날쭉했다). */
197
+ .manager-section--grid {
198
+ margin-bottom: 0;
199
+ min-width: 0;
200
+ display: flex;
201
+ flex-direction: column;
202
+ }
203
+ .manager-section--grid > .manager-bar-chart-wrap,
204
+ .manager-section--grid > .manager-bar-chart-loading,
205
+ .manager-section--grid > .ring-chart-section-body {
206
+ margin-top: auto;
207
+ }
@@ -31,6 +31,13 @@ export interface ChartSectionProps {
31
31
  subRow?: ReactNode;
32
32
  /** 섹션 루트에 추가할 클래스 */
33
33
  className?: string;
34
+ /**
35
+ * CSS 격자(한 줄에 여러 칸) 안에 세울 때 켠다 — `manager-section--grid`.
36
+ * 아래 여백(14px)·최소 폭(320px)을 풀고 카드를 세로 flex 로 만들어, 같은 줄 카드끼리 높이가 늘어나면
37
+ * 차트 몸통(막대·링·로딩)을 **카드 바닥**에 맞춘다(범례 줄 수가 달라도 막대 바닥이 한 선).
38
+ * 차트가 아닌 카드에 같은 꼴이 필요하면 `manager-section manager-section--grid` 클래스를 그대로 쓴다.
39
+ */
40
+ inGrid?: boolean;
34
41
  children?: ReactNode;
35
42
  }
36
43
  /**
@@ -39,4 +46,4 @@ export interface ChartSectionProps {
39
46
  * 공유하도록 두 컴포넌트가 내부적으로 사용한다.
40
47
  * @public
41
48
  */
42
- export default function ChartSection({ title, titleClassName, headRight, subRow, className, children }: ChartSectionProps): import("react").JSX.Element;
49
+ export default function ChartSection({ title, titleClassName, headRight, subRow, className, inGrid, children }: ChartSectionProps): import("react").JSX.Element;
@@ -17,6 +17,6 @@ export function ChartTotal({ label, children }) {
17
17
  * 공유하도록 두 컴포넌트가 내부적으로 사용한다.
18
18
  * @public
19
19
  */
20
- export default function ChartSection({ title, titleClassName = 'h3', headRight, subRow, className, children }) {
21
- return (_jsxs("section", { className: className ? `manager-section ${className}` : 'manager-section', children: [_jsxs("div", { className: "manager-section__head", children: [_jsx("div", { className: titleClassName, children: title }), headRight] }), subRow && _jsx("div", { className: "barchart-sub-row", children: subRow }), children] }));
20
+ export default function ChartSection({ title, titleClassName = 'h3', headRight, subRow, className, inGrid = false, children }) {
21
+ return (_jsxs("section", { className: ['manager-section', inGrid && 'manager-section--grid', className].filter(Boolean).join(' '), children: [_jsxs("div", { className: "manager-section__head", children: [_jsx("div", { className: titleClassName, children: title }), headRight] }), subRow && _jsx("div", { className: "barchart-sub-row", children: subRow }), children] }));
22
22
  }
@@ -4,5 +4,5 @@ import './ChoiceChips.css';
4
4
  /** @public */
5
5
  export default function ChoiceChips({ subs = [], selected, onSelect, size = 'medium' }) {
6
6
  const buttonSize = size === 'small' ? 32 : 40;
7
- return (_jsx("div", { className: `choice-chips choice-chips--${size}`, children: _jsx("div", { className: "choice-chips__list", children: subs.map((s) => (_jsx(Button, { size: buttonSize, type: "bar", label: s.label, selected: selected === s.key, className: "choice-chips__button", onClick: () => onSelect?.(s.key) }, s.key))) }) }));
7
+ return (_jsx("div", { className: `choice-chips choice-chips--${size}`, children: _jsx("div", { className: "choice-chips__list", children: subs.map((s) => (_jsx(Button, { htmlType: "button", size: buttonSize, type: "bar", label: s.label, selected: selected === s.key, className: "choice-chips__button", onClick: () => onSelect?.(s.key) }, s.key))) }) }));
8
8
  }
@@ -128,5 +128,5 @@ export default function CodeBox({ children, language = 'bash', title = '', showC
128
128
  };
129
129
  const tokens = tokenize(codeText, language);
130
130
  const tokenTypo = size === 'small' ? 'caption-1' : 'body-3';
131
- return (_jsxs("div", { className: cx('code-box', size === 'small' ? 'code-box--small' : '', className), children: [(title || showCopy) && (_jsxs("div", { className: "code-box__header", role: "toolbar", "aria-label": t('common.codeBlock', '코드 블록'), children: [_jsx("div", { className: "code-box__title caption-1", children: title }), showCopy && (_jsx(Button, { className: "code-box__copy", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: copied ? 'check' : 'copy', onClick: handleCopy, ariaLabel: copied ? t('common.copied', '복사됨') : t('common.copyCode', '코드 복사') }))] })), _jsx("pre", { className: "code-box__inner", "data-lang": language, children: _jsx("code", { children: tokens.map((t, i) => (_jsx("span", { className: `code-token ${t.type} ${tokenTypo}`, children: t.content }, i))) }) })] }));
131
+ return (_jsxs("div", { className: cx('code-box', size === 'small' ? 'code-box--small' : '', className), children: [(title || showCopy) && (_jsxs("div", { className: "code-box__header", role: "toolbar", "aria-label": t('common.codeBlock', '코드 블록'), children: [_jsx("div", { className: "code-box__title caption-1", children: title }), showCopy && (_jsx(Button, { htmlType: "button", className: "code-box__copy", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: copied ? 'check' : 'copy', onClick: handleCopy, ariaLabel: copied ? t('common.copied', '복사됨') : t('common.copyCode', '코드 복사') }))] })), _jsx("pre", { className: "code-box__inner", "data-lang": language, children: _jsx("code", { children: tokens.map((t, i) => (_jsx("span", { className: `code-token ${t.type} ${tokenTypo}`, children: t.content }, i))) }) })] }));
132
132
  }
@@ -95,8 +95,10 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
95
95
  if (!open)
96
96
  return;
97
97
  const 판 = menuRef.current;
98
- const 갈곳 = 판?.querySelector('.date-picker__day--selected') ??
99
- 판?.querySelector('.date-picker__day--today') ??
98
+ const 갈곳 = 판?.querySelector('.date-picker__day--selected:not(:disabled)') ??
99
+ 판?.querySelector('.date-picker__day--today:not(:disabled)') ??
100
+ /* 오늘이 min/max 밖이라 꺼져 있으면 focus() 가 실패해 트리거에 남았다 — 켜진 첫 날로 */
101
+ 판?.querySelector('.date-picker__day:not(:disabled)') ??
100
102
  판?.querySelector(FOCUSABLE_SELECTOR) ??
101
103
  판;
102
104
  갈곳?.focus?.();
@@ -199,6 +201,17 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
199
201
  return true;
200
202
  return false;
201
203
  };
204
+ /* Tab 을 받을 날 — 활성일이 min/max 밖이라 꺼져 있으면 켜진 첫 날로. 꺼진 단추는 Tab 을 못 받아
205
+ 격자에 Tab 자리가 **하나도 없어** 키보드로 달력에 들어갈 수 없었다(값 없음 + min=내일). */
206
+ const 탭자리 = (() => {
207
+ const 끝 = daysInMonth(viewY, viewM);
208
+ if (활성일 >= 1 && 활성일 <= 끝 && !isDisabledDay(활성일))
209
+ return 활성일;
210
+ for (let d = 1; d <= 끝; d++)
211
+ if (!isDisabledDay(d))
212
+ return d;
213
+ return -1;
214
+ })();
202
215
  const cells = [];
203
216
  for (let i = 0; i < firstDay(viewY, viewM); i++)
204
217
  cells.push(null);
@@ -218,7 +231,7 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
218
231
  const isToday = iso === toISO(today.getFullYear(), today.getMonth(), today.getDate());
219
232
  const dis = isDisabledDay(d);
220
233
  const col = (i % 7);
221
- return (_jsx("button", { type: "button", disabled: dis, "data-day": d, tabIndex: d === 활성일 ? 0 : -1, "aria-current": isToday ? 'date' : undefined, onClick: () => !dis && selectDay(d), className: cx('date-picker__day caption-1', isSelected && 'date-picker__day--selected', isToday && !isSelected && 'date-picker__day--today', dis && 'date-picker__day--disabled', col === 0 && 'date-picker__day--sun', col === 6 && 'date-picker__day--sat'), children: d }, d));
234
+ return (_jsx("button", { type: "button", disabled: dis, "data-day": d, tabIndex: d === 탭자리 ? 0 : -1, "aria-current": isToday ? 'date' : undefined, onClick: () => !dis && selectDay(d), className: cx('date-picker__day caption-1', isSelected && 'date-picker__day--selected', isToday && !isSelected && 'date-picker__day--today', dis && 'date-picker__day--disabled', col === 0 && 'date-picker__day--sun', col === 6 && 'date-picker__day--sat'), children: d }, d));
222
235
  })] })] }), document.body) : null;
223
236
  return (_jsxs("div", { className: cx('date-picker', `date-picker--s${size}`, open && 'date-picker--open', className), children: [_jsxs("button", { ref: triggerRef, type: "button", className: "date-picker__trigger body-2", onClick: () => !disabled && toggle(), disabled: disabled, "aria-haspopup": "true", "aria-expanded": open, children: [_jsx("span", { className: value ? 'date-picker__value' : 'date-picker__placeholder', children: value ? formatDisplay(value) : placeholder }), _jsx(Calendar, { size: iconSize, className: "date-picker__icon", strokeWidth: 1.5 })] }), dropdown] }));
224
237
  }
@@ -54,7 +54,10 @@ export default function Dropdown({ options, value, onChange, placeholder, disabl
54
54
  }
55
55
  });
56
56
  }
57
- }, [open, options, value, menuRef]);
57
+ /* **열릴 때만** 돈다. options·value 까지 걸어 두면, 열린 동안 부모가 다시 그려지며 options 를 인라인 배열로
58
+ 넘길 때마다 화살표로 옮겨 둔 가리킴이 선택값 자리로 되돌아가고 스크롤도 튀었다. */
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, [open]);
58
61
  /* eslint-enable react-hooks/set-state-in-effect */
59
62
  const toggle = useCallback(() => { if (!disabled)
60
63
  rawToggle(); }, [disabled, rawToggle]);
@@ -102,13 +105,20 @@ export default function Dropdown({ options, value, onChange, placeholder, disabl
102
105
  select(options[activeIdx]);
103
106
  }
104
107
  }, [disabled, open, options, activeIdx, select, setOpen]);
105
- const menu = open ? (_jsx("div", { ref: menuRef, id: listId, className: "dropdown__menu", role: "listbox", style: menuStyle, onMouseLeave: handleMenuLeave, children: options.map((opt, i) => (_jsxs("button", { id: `${listId}-option-${i}`, type: "button", role: "option", "aria-selected": opt.value === value, className: cx('dropdown__option body-2', opt.value === value ? 'dropdown__option--active' : '', i === activeIdx ? 'dropdown__option--active' : '', opt.disabled ? 'dropdown__option--disabled' : ''),
108
+ const menu = open ? (_jsx("div", { ref: menuRef, id: listId, className: "dropdown__menu", role: "listbox", style: menuStyle, onMouseLeave: handleMenuLeave,
109
+ /* 메뉴 빈 곳·스크롤 막대를 눌러도 트리거에서 포커스가 안 떠나게 — 아래 트리거 onBlur 가 메뉴를 닫지 않도록 */
110
+ onMouseDown: e => e.preventDefault(), children: options.map((opt, i) => (_jsxs("button", { id: `${listId}-option-${i}`, type: "button", role: "option", "aria-selected": opt.value === value, className: cx('dropdown__option body-2', opt.value === value ? 'dropdown__option--active' : '', i === activeIdx ? 'dropdown__option--active' : '', opt.disabled ? 'dropdown__option--disabled' : ''),
106
111
  // 선택은 click 에서 한다. onMouseDown 만 두면 마우스로는 눌리지만 스크린리더가
107
112
  // 항목을 활성화할 때는 click 만 발생해 **아무 일도 일어나지 않는다**.
108
113
  // mousedown 에서는 기본 동작(포커스 이동 → 트리거 blur → 메뉴 닫힘)만 막아,
109
114
  // click 이 도달하기 전에 메뉴가 사라지지 않게 한다.
110
115
  onMouseDown: e => e.preventDefault(), onClick: () => select(opt), onMouseEnter: () => setActiveIdx(i), disabled: opt.disabled, children: [_jsx("span", { className: "dropdown__option-check", children: opt.value === value && (_jsx("svg", { viewBox: "0 0 14 14", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "2 7 5.5 10.5 12 3.5" }) })) }), opt.label] }, opt.value))) })) : null;
111
- return (_jsxs("div", { ref: containerRef, className: cx('dropdown', `dropdown--s${size}`, open ? 'dropdown--open' : '', block ? 'dropdown--block' : '', error ? 'dropdown--error' : '', className), onKeyDown: handleKeyDown, children: [_jsxs("button", { ref: triggerRef, type: "button", className: "dropdown__trigger", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listId : undefined, "aria-activedescendant": open && activeIdx >= 0 ? `${listId}-option-${activeIdx}` : undefined, "aria-label": ariaLabel, "aria-invalid": error || undefined, disabled: disabled, onClick: toggle, children: [_jsx("span", { className: `dropdown__trigger-label body-2${!selected ? ' dropdown__trigger-label--placeholder' : ''}`, children: selected ? selected.label : resolvedPlaceholder }), _jsx("svg", { className: "dropdown__chevron", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "4 6 8 10 12 6" }) })] }), typeof document !== 'undefined' && createPortal(menu,
116
+ return (_jsxs("div", { ref: containerRef, className: cx('dropdown', `dropdown--s${size}`, open ? 'dropdown--open' : '', block ? 'dropdown--block' : '', error ? 'dropdown--error' : '', className), onKeyDown: handleKeyDown, children: [_jsxs("button", { ref: triggerRef, type: "button", className: "dropdown__trigger", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listId : undefined, "aria-activedescendant": open && activeIdx >= 0 ? `${listId}-option-${activeIdx}` : undefined, "aria-label": ariaLabel, "aria-invalid": error || undefined, disabled: disabled, onClick: toggle,
117
+ /* Tab 으로 포커스가 떠나면 닫는다 — 포커스는 트리거에 두고 aria-activedescendant 로 가리키는 목록이라, 떠난 뒤
118
+ 열린 채 남으면 화살표·Enter 가 더는 메뉴로 가지 않는 죽은 메뉴가 된다. 항목·메뉴를 누르는 것은 위에서
119
+ mousedown 기본 동작을 막아 blur 가 나지 않는다. */
120
+ onBlur: e => { if (open && !menuRef.current?.contains(e.relatedTarget))
121
+ setOpen(false); }, children: [_jsx("span", { className: `dropdown__trigger-label body-2${!selected ? ' dropdown__trigger-label--placeholder' : ''}`, children: selected ? selected.label : resolvedPlaceholder }), _jsx("svg", { className: "dropdown__chevron", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "4 6 8 10 12 6" }) })] }), typeof document !== 'undefined' && createPortal(menu,
112
122
  // 코어 CSS를 .ihabdevteam-core-scope 로 스코프한 소비자(플레이그라운드 등)에서도
113
123
  // body로 portal된 메뉴가 스타일을 잃지 않도록, 가까운 scope 안으로 portal
114
124
  // createPortal의 target은 렌더링 시점에 동기적으로 결정되어야 하므로 ref 읽기를 effect로 옮길 수 없다
@@ -92,16 +92,17 @@ export default function FileUpload({ onFilesSelected, accept, multiple = false,
92
92
  inputRef.current?.click();
93
93
  };
94
94
  const handleRemove = (index) => {
95
- if (onRemove) {
96
- onRemove(index);
97
- return;
98
- }
95
+ /* onRemove 를 주면 알림은 그쪽 한 번으로 끝낸다(onFilesSelected 로 이중으로 알리지 않는다). 다만 비제어 모드에서는
96
+ **보이는 목록도 줄인다** — 전에는 줄이기 전에 돌아가 버려 뺀 파일이 화면에 남았다. 제어 모드(files 를 부모가
97
+ 쥠)는 부모가 onRemove 에서 줄인다. */
98
+ onRemove?.(index);
99
99
  if (controlledFiles === undefined) {
100
- /* 뺐다는 것도 알린다. 목록만 줄이고 가만히 있으면 **호출부 손에는 그 파일이
101
- 그대로 남아** 사용자가 뺀 것이 올라간다. */
102
100
  const next = files.filter((_, i) => i !== index);
103
101
  setInternalFiles(next);
104
- onFilesSelected?.(next);
102
+ /* onRemove 가 없으면 뺐다는 것을 이 길로 알린다. 목록만 줄이고 가만히 있으면 **호출부 손에는 그 파일이
103
+ 그대로 남아** 사용자가 뺀 것이 올라간다. */
104
+ if (!onRemove)
105
+ onFilesSelected?.(next);
105
106
  }
106
107
  };
107
108
  return (_jsxs("div", { className: `file-upload ${className}`.trim(), children: [_jsxs("div", { className: cx('file-upload__dropzone', isDragging && 'file-upload__dropzone--dragging', disabled && 'file-upload__dropzone--disabled'), role: "button", tabIndex: disabled ? -1 : 0, "aria-disabled": disabled, onClick: handleBrowseClick, onKeyDown: e => { if (e.key === 'Enter' || e.key === ' ') {