@ihabdevteam/core 0.130.0 → 0.132.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 (43) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/components.cjs.js +176 -176
  3. package/dist/components.esm.css +2 -2
  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 +69 -20
  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 +24 -13
  13. package/dist/packages/core/src/components/BottomNav.css +15 -0
  14. package/dist/packages/core/src/components/BottomNav.d.ts +6 -0
  15. package/dist/packages/core/src/components/BottomNav.js +12 -1
  16. package/dist/packages/core/src/components/Calendar.js +4 -0
  17. package/dist/packages/core/src/components/ChartSection.css +16 -0
  18. package/dist/packages/core/src/components/ChartSection.d.ts +8 -1
  19. package/dist/packages/core/src/components/ChartSection.js +2 -2
  20. package/dist/packages/core/src/components/Header.js +3 -1
  21. package/dist/packages/core/src/components/PageTitle.css +6 -0
  22. package/dist/packages/core/src/components/PageTitle.d.ts +9 -1
  23. package/dist/packages/core/src/components/PageTitle.js +2 -2
  24. package/dist/packages/core/src/components/PassCard/PassCardBg.css +33 -0
  25. package/dist/packages/core/src/components/Popup/Popup.d.ts +8 -0
  26. package/dist/packages/core/src/components/Popup/Popup.js +4 -2
  27. package/dist/packages/core/src/components/RingChart.d.ts +3 -1
  28. package/dist/packages/core/src/components/RingChart.js +2 -2
  29. package/dist/packages/core/src/components/SideNav.js +27 -10
  30. package/dist/packages/core/src/components/Table.d.ts +17 -1
  31. package/dist/packages/core/src/components/Table.js +15 -9
  32. package/dist/packages/core/src/components/TextField.js +34 -5
  33. package/dist/packages/core/src/components/barChartTypes.d.ts +2 -0
  34. package/dist/packages/core/src/components/useComboboxSuggestions.d.ts +1 -0
  35. package/dist/packages/core/src/components/useComboboxSuggestions.js +12 -6
  36. package/dist/packages/core/src/i18n/locales/en.json +2 -1
  37. package/dist/packages/core/src/i18n/locales/ko.json +2 -1
  38. package/dist/packages/core/src/utils/CategoryData.d.ts +12 -40
  39. package/dist/packages/core/src/utils/CategoryData.js +13 -8
  40. package/dist/packages/core/src/version.d.ts +1 -1
  41. package/dist/packages/core/src/version.js +1 -1
  42. package/dist/utils.cjs.js +1 -1
  43. 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.130.0";
1372
+ export declare const CORE_VERSION = "0.132.0";
1359
1373
 
1360
1374
  /** @public */
1361
1375
  declare const coreDefault: Record<string, unknown>;
@@ -3017,6 +3031,12 @@ export declare type NavItem = {
3017
3031
  label?: string;
3018
3032
  icon?: string;
3019
3033
  state?: string;
3034
+ /**
3035
+ * 단추 우상단 원형 숫자 배지(SideNav 항목 배지와 같은 모양). 0/undefined 면 숨기고, 99 에서 자른다.
3036
+ * 배지는 낭독기에서 숨기고 개수는 단추 이름에 붙인다. 넘쳐 "…" 메뉴로 가면 메뉴 항목의 배지가 되고,
3037
+ * "…" 단추에는 넘친 항목 개수의 합이 선다.
3038
+ */
3039
+ badgeCount?: number;
3020
3040
  };
3021
3041
 
3022
3042
  /**
@@ -3240,7 +3260,7 @@ export declare type PageProps = {
3240
3260
  export declare function pageStartIndexes(pageLengths: readonly number[]): number[];
3241
3261
 
3242
3262
  /** @public */
3243
- 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;
3244
3264
 
3245
3265
  declare type PageTitleProps = {
3246
3266
  title: ReactNode;
@@ -3271,6 +3291,14 @@ declare type PageTitleProps = {
3271
3291
  * title 이 문자열이면 이 값은 필요 없다.
3272
3292
  */
3273
3293
  documentTitle?: string;
3294
+ /**
3295
+ * 오른쪽(`rightAction`)이 넓어 제목 칸을 짓누를 때 **줄을 접어** 오른쪽을 아래로 내린다. 기본 false(한 줄 유지).
3296
+ *
3297
+ * 한 줄로 두면 제목 칸이 `min-width: 0` 까지 줄어, 탭 일곱 같은 넓은 오른쪽이 제목을 한 글자씩 세로로
3298
+ * 밀어낸다(말귀 고객 상세 1135px 실측). 켜면 제목 칸에 320px(좁으면 제 폭) 바닥을 주고, 그 바닥이
3299
+ * 안 들어가면 오른쪽이 다음 줄로 간다.
3300
+ */
3301
+ wrap?: boolean;
3274
3302
  };
3275
3303
 
3276
3304
  /**
@@ -4119,6 +4147,14 @@ export declare interface PopupProps {
4119
4147
  * 더 넓거나 무제한이 필요하면 className으로 `.popup-panel` max-width를 직접 override.
4120
4148
  */
4121
4149
  size?: 'small' | 'medium' | 'large';
4150
+ /**
4151
+ * 패널 높이를 못 박는다(숫자는 px, 문자열은 CSS 길이 그대로). 미지정 시 내용만큼(기본).
4152
+ *
4153
+ * 탭을 옮길 때마다 내용 높이가 달라 창이 위아래로 뛰는 팝업에 쓴다. **화면보다 크면 화면에
4154
+ * 맞춘다** — 패널의 `max-height`(데스크톱 `100% - 60px`, 좁은 화면은 하단 내비 몫까지 뺀 값)가
4155
+ * 그대로 걸리므로 창이 화면 밖으로 나가지 않는다. 넘치는 내용은 본문이 스스로 굴린다.
4156
+ */
4157
+ height?: number | string;
4122
4158
  /**
4123
4159
  * 이 값이 바뀌면 본문(.popup-content) 스크롤을 맨 위로 되돌린다.
4124
4160
  * 탭/모드 전환처럼 내부 콘텐츠가 통째로 바뀌는 팝업에서, 스크롤을 내린 채
@@ -4898,7 +4934,7 @@ export declare function reviewTableName(original: string, today: Date): string;
4898
4934
  * 미지정 시에는 헤더 없는 순수 링만 렌더된다(크기/색상 데모 등).
4899
4935
  * @public
4900
4936
  */
4901
- 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;
4902
4938
 
4903
4939
  /** @public */
4904
4940
  export declare interface RingChartProps {
@@ -4949,6 +4985,8 @@ export declare interface RingChartProps {
4949
4985
  loading?: boolean;
4950
4986
  /** 섹션 루트에 추가할 클래스 (titleKey 지정 시에만 사용) */
4951
4987
  className?: string;
4988
+ /** CSS 격자 칸 안에 세운다 — 아래 여백·최소 폭을 풀고 링을 카드 바닥에 맞춘다. ChartSection `inGrid` 참고(titleKey 지정 시에만 사용) */
4989
+ inGrid?: boolean;
4952
4990
  }
4953
4991
 
4954
4992
  /** @public */
@@ -6011,7 +6049,7 @@ export declare type TabItem = {
6011
6049
  /** @public */
6012
6050
  export declare function Table<T extends {
6013
6051
  id?: string | number;
6014
- }>({ 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;
6015
6053
 
6016
6054
  /**
6017
6055
  * **다음에 쓸 표를 고른다.**
@@ -6133,12 +6171,28 @@ export declare type TableProps<T extends {
6133
6171
  style?: CSSProperties;
6134
6172
  }>;
6135
6173
  onRowClick?: (item: T) => void;
6174
+ /**
6175
+ * 줄마다 누를 수 있는지. false 인 줄은 `onRowClick` 이 안 불리고, 버튼으로 읽히지도(role·Tab 정지점) 않으며,
6176
+ * 손가락 커서·hover 강조도 없다. `onRowClick` 이 있을 때만 뜻이 있다. 미지정이면 모든 줄이 눌린다(기본).
6177
+ *
6178
+ * 예: 방문 기록 표에서 '안 옴' 줄은 열 것이 없다 — CSS 로 커서만 되돌리면 눌리기는 그대로이고 낭독기는 여전히 버튼이라 한다.
6179
+ */
6180
+ isRowClickable?: (item: T) => boolean;
6136
6181
  onRowMouseEnter?: (item: T, event: MouseEvent_2<HTMLDivElement>) => void;
6137
6182
  onRowMouseLeave?: (item: T, event: MouseEvent_2<HTMLDivElement>) => void;
6138
6183
  onRowDragStart?: (item: T, event: DragEvent_2<HTMLDivElement>) => void;
6139
6184
  onRowDragEnd?: (item: T, event: DragEvent_2<HTMLDivElement>) => void;
6140
6185
  draggingId?: string | number | null;
6141
6186
  gridTemplateColumns?: string;
6187
+ /**
6188
+ * 좁은 화면(767px 이하)에서 쓸 열 틀. 주면 그 폭에서는 이 틀을 쓰고 **끌어 둔 폭(저장값)은 무시**한다 —
6189
+ * 폭 조절 손잡이와 '칸 너비 초기화' 단추도 숨겨, 좁은 화면에서 끈 값이 넓은 화면의 저장값을 덮지 않게 한다.
6190
+ * 넓은 화면으로 돌아오면 저장값이 그대로 돌아온다. 미지정이면 좁은 화면도 넓은 화면과 같다(기본).
6191
+ *
6192
+ * 열 틀은 인라인 CSS 변수로 들어가므로 앱 CSS 로는 `!important` 없이 못 이긴다 — 그래서 prop 이다.
6193
+ * 숨길 칸은 `renderRow` 의 `className`·`fields[].className` 으로 앱이 가린다.
6194
+ */
6195
+ mobileGridTemplateColumns?: string;
6142
6196
  /** true면 헤더 경계선을 드래그해 컬럼 폭 조절 가능 */
6143
6197
  resizable?: boolean;
6144
6198
  /**
@@ -6641,28 +6695,22 @@ export declare interface TopBannerProps {
6641
6695
  'aria-label'?: string;
6642
6696
  }
6643
6697
 
6644
- /** @public */
6645
- export declare const TOPIC_TYPES: ({
6646
- key: string;
6647
- label: string;
6648
- icon: string;
6649
- color: string;
6650
- subs: {
6651
- key: string;
6652
- label: string;
6653
- }[];
6654
- desc?: undefined;
6655
- } | {
6698
+ /**
6699
+ * 주제·세부 주제 목록(열쇠·이름·아이콘·색). **설명(desc)은 두지 않는다** — 코어·앱 어디에서도 주제 설명을
6700
+ * 그리지 않고, 한때 7개 주제에 붙어 있던 '소음 환경 속 문장 이해 훈련' 은 훈련 유형(소음 하 문장 듣기)의
6701
+ * 설명을 옮겨 붙인 값이었다(2026-09-14 걷음). 화면에 보이는 설명은 훈련 유형(`TRAINING_TYPES`)의 것이다.
6702
+ * @public
6703
+ */
6704
+ export declare const TOPIC_TYPES: {
6656
6705
  key: string;
6657
6706
  label: string;
6658
6707
  icon: string;
6659
- desc: string;
6660
6708
  color: string;
6661
6709
  subs: {
6662
6710
  key: string;
6663
6711
  label: string;
6664
6712
  }[];
6665
- })[];
6713
+ }[];
6666
6714
 
6667
6715
  /**
6668
6716
  * 훈련 카테고리 데이터 DI — 앱에서 주입하는 함수 기반 인터페이스.
@@ -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
  };
@@ -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; // 아직 도래하지 않은 날 — 글씨 흐리게
@@ -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, { 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로 분리
@@ -38,6 +38,21 @@
38
38
  overflow: hidden;
39
39
  }
40
40
 
41
+ /* 단추 한 칸 — 단추와 같은 52px 이라 넘침 계산이 그대로다. 배지(.circle-badge--top-right)의 기준.
42
+ 배지는 Badge 기본 코너(-6px)가 아니라 칸 안쪽 모서리(0)에 둔다 — .bottom-nav__items 가
43
+ overflow: hidden 이라 바깥으로 나간 6px 가 잘린다(넘침 계산을 위해 그 hidden 은 둬야 한다). */
44
+ .bottom-nav__slot {
45
+ position: relative;
46
+ display: flex;
47
+ flex-shrink: 0;
48
+ width: 52px;
49
+ height: 52px;
50
+ }
51
+ .bottom-nav__slot .circle-badge--top-right {
52
+ top: 0;
53
+ right: 0;
54
+ }
55
+
41
56
  .bottom-nav__right {
42
57
  display: flex;
43
58
  align-items: center;
@@ -6,6 +6,12 @@ export type NavItem = {
6
6
  label?: string;
7
7
  icon?: string;
8
8
  state?: string;
9
+ /**
10
+ * 단추 우상단 원형 숫자 배지(SideNav 항목 배지와 같은 모양). 0/undefined 면 숨기고, 99 에서 자른다.
11
+ * 배지는 낭독기에서 숨기고 개수는 단추 이름에 붙인다. 넘쳐 "…" 메뉴로 가면 메뉴 항목의 배지가 되고,
12
+ * "…" 단추에는 넘친 항목 개수의 합이 선다.
13
+ */
14
+ badgeCount?: number;
9
15
  };
10
16
  /**
11
17
  * 항상 "더보기(…)" 메뉴에만 노출되는 유틸 항목(알림·설정·계정 등).
@@ -4,6 +4,7 @@ import { useCallback, useId, useMemo, useState, useRef, useEffect } from 'react'
4
4
  import { createPortal } from 'react-dom';
5
5
  import './BottomNav.css';
6
6
  import Button from './Button';
7
+ import Badge from './Badge';
7
8
  import MenuTooltip from './Tooltip/MenuTooltip';
8
9
  // .tooltip-content(메뉴 표면)를 직접 쓰므로 정의 파일을 명시적으로 가져온다.
9
10
  // Tooltip.tsx만 이 CSS를 import하는데 BottomNav는 그 컴포넌트를 거치지 않아,
@@ -13,6 +14,8 @@ import { useAnchoredMenu } from '../hooks/useAnchoredMenu';
13
14
  import { useTranslation } from 'react-i18next';
14
15
  const BTN_SIZE = 52;
15
16
  const BTN_GAP = 10;
17
+ /** 배지 숫자 — SideNav·MenuTooltip 과 같이 99 에서 자른다. 0 이하·없음은 0. */
18
+ const badgeOf = (n) => (n && n > 0 ? n : 0);
16
19
  /** @public */
17
20
  export default function BottomNav({ items = [], activeKey, onChange, appMode, onAppModeChange, moreItems = [], }) {
18
21
  const { t } = useTranslation();
@@ -59,6 +62,8 @@ export default function BottomNav({ items = [], activeKey, onChange, appMode, on
59
62
  onScroll: 'ignore',
60
63
  onResize: 'ignore',
61
64
  });
65
+ /* 단추 이름에 개수를 붙인다 — 배지는 aria-hidden 이라 개수는 이름으로만 전해진다. */
66
+ const nameWithCount = useCallback((label, count) => count > 0 ? t('nav.badgeLabel', { label, count, defaultValue: '{{label}}, {{count}}개' }) : label, [t]);
62
67
  const handleModeChange = useCallback(() => onAppModeChange?.(nextMode), [onAppModeChange, nextMode]);
63
68
  // 넘친 nav 항목 또는 유틸 항목이 현재 활성 상태면 … 버튼에 그 아이콘/선택표시를 반영
64
69
  const activeMore = useMemo(() => hiddenItems.find(it => it.key === activeKey) ?? moreItems.find(it => it.selected || (it.key && it.key === activeKey)), [hiddenItems, activeKey, moreItems]);
@@ -68,6 +73,7 @@ export default function BottomNav({ items = [], activeKey, onChange, appMode, on
68
73
  ...hiddenItems.map(it => ({
69
74
  label: it.label,
70
75
  icon: it.icon,
76
+ badgeCount: it.badgeCount,
71
77
  selected: it.key === activeKey,
72
78
  closeOnSelect: true,
73
79
  onClick: () => {
@@ -89,5 +95,10 @@ export default function BottomNav({ items = [], activeKey, onChange, appMode, on
89
95
  })),
90
96
  ]
91
97
  }], [hiddenItems, moreItems, activeKey, onChange, handleCloseMore]);
92
- 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) => (_jsx(Button, { size: 52, type: "dot", icon: it.icon, ariaLabel: it.label, selected: it.key === activeKey, disabled: it.state === 'disabled', className: "bottom-nav-item", onClick: () => it.key && onChange?.(it.key) }, it.key))) }), showMore && (_jsxs(_Fragment, { children: [_jsx("div", { className: "bottom-nav__divider", "aria-hidden": "true" }), _jsx("div", { className: "bottom-nav__right", children: _jsxs("div", { ref: moreRef, children: [_jsx(Button, { size: 52, type: "dot", icon: activeMore?.icon ?? 'ellipsis', ariaLabel: activeMore?.label ?? t('nav.more', '더보기'), selected: !!activeMore, className: "bottom-nav-item", onClick: handleToggleMore, "aria-expanded": moreOpen, "aria-controls": moreOpen ? moreMenuId : undefined }), 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)] }) })] }))] }));
98
+ // "…" 단추 배지 — 메뉴 안에서만 보이는 개수(넘친 항목 + 유틸)를 닫힌 채로도 알린다.
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) => {
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)] }) })] }))] }));
93
104
  }
@@ -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
  }
@@ -47,7 +47,8 @@ export default function Header({ activeKey, onChange, items, malgwiItems: _malgw
47
47
  캐스팅만 해 넘겨, 두 타입의 이름이 다른 칸이 조용히 버려졌다 — `disabled` 가 `state` 로
48
48
  안 가서 **비활성 항목이 모바일에서 눌렸고**, `href` 항목은 새 창 대신 `onChange(key)` 로
49
49
  갔다. SideNav 가 거르는 구획(section)·빈 항목도 같이 거른다.
50
- (`badge` 는 BottomNav 에 받을 자리가 없어 아직 못 옮긴다.) */
50
+ `badge` 는 `badgeCount` 로 옮긴다 — BottomNav 가 단추 모서리(넘치면 "…" 단추와 메뉴 항목)에
51
+ SideNav 와 같은 원형 숫자 배지로 그린다. 예전엔 받을 자리가 없어 숙제 개수가 모바일에서 안 보였다. */
51
52
  const bottomItems = useMemo(() => (items ?? [])
52
53
  .filter(it => !it.section && (it.key || it.label))
53
54
  .map(it => ({
@@ -55,6 +56,7 @@ export default function Header({ activeKey, onChange, items, malgwiItems: _malgw
55
56
  label: typeof it.label === 'string' ? it.label : undefined,
56
57
  icon: it.icon,
57
58
  state: it.disabled ? 'disabled' : undefined,
59
+ badgeCount: it.badge,
58
60
  })), [items]);
59
61
  const handleBottomChange = useCallback((key) => {
60
62
  const it = items?.find(i => i.key === key);
@@ -32,3 +32,9 @@
32
32
  음수 여백으로 왼쪽을 당겨, 아이콘 단추의 제 여백 때문에 제목이 페이지의 다른
33
33
  제목들보다 오른쪽으로 밀려 보이는 것을 막는다(글자 시작선을 맞춘다). */
34
34
  .page-title__back { margin-left: -8px; margin-right: -4px; flex-shrink: 0; }
35
+
36
+ /* wrap — 오른쪽이 넓으면 줄을 접는다. 제목 칸의 바닥을 320px 로 두되 **제 그릇보다 크지 않게**(min) —
37
+ 그냥 320px 이면 좁은 화면(세로 배치, 그릇 280px)에서 제목이 그릇을 넘쳐 오른쪽이 잘린다
38
+ (ChartSection 의 min-width 320px 가 똑같이 그랬다). */
39
+ .page-title--wrap { flex-wrap: wrap; }
40
+ .page-title--wrap .page-title__main { min-width: min(320px, 100%); }
@@ -29,7 +29,15 @@ type PageTitleProps = {
29
29
  * title 이 문자열이면 이 값은 필요 없다.
30
30
  */
31
31
  documentTitle?: string;
32
+ /**
33
+ * 오른쪽(`rightAction`)이 넓어 제목 칸을 짓누를 때 **줄을 접어** 오른쪽을 아래로 내린다. 기본 false(한 줄 유지).
34
+ *
35
+ * 한 줄로 두면 제목 칸이 `min-width: 0` 까지 줄어, 탭 일곱 같은 넓은 오른쪽이 제목을 한 글자씩 세로로
36
+ * 밀어낸다(말귀 고객 상세 1135px 실측). 켜면 제목 칸에 320px(좁으면 제 폭) 바닥을 주고, 그 바닥이
37
+ * 안 들어가면 오른쪽이 다음 줄로 간다.
38
+ */
39
+ wrap?: boolean;
32
40
  };
33
41
  /** @public */
34
- export default function PageTitle({ title, sub, rightAction, titleAction, badge, badgeVariant, documentTitle, onBack, backLabel }: PageTitleProps): import("react").JSX.Element;
42
+ export default function PageTitle({ title, sub, rightAction, titleAction, badge, badgeVariant, documentTitle, onBack, backLabel, wrap }: PageTitleProps): import("react").JSX.Element;
35
43
  export {};
@@ -5,12 +5,12 @@ import Badge from './Badge';
5
5
  import Button from './Button';
6
6
  import { useDocumentTitle } from '../hooks/useDocumentTitle';
7
7
  /** @public */
8
- export default function PageTitle({ title, sub, rightAction, titleAction, badge, badgeVariant = 'default', documentTitle, onBack, backLabel }) {
8
+ export default function PageTitle({ title, sub, rightAction, titleAction, badge, badgeVariant = 'default', documentTitle, onBack, backLabel, wrap = false }) {
9
9
  const { t } = useTranslation();
10
10
  // 페이지 제목이 곧 문서 제목이 되도록 맞춘다(WCAG 2.4.2).
11
11
  useDocumentTitle(typeof title === 'string' ? title : (documentTitle ?? null));
12
12
  const 뒤로이름 = backLabel ?? t('common.back', '뒤로');
13
- return (_jsxs("div", { className: "page-title", children: [_jsxs("div", { className: "page-title__main", children: [_jsxs("div", { className: "page-title__title-row", children: [onBack && (
13
+ return (_jsxs("div", { className: wrap ? 'page-title page-title--wrap' : 'page-title', children: [_jsxs("div", { className: "page-title__main", children: [_jsxs("div", { className: "page-title__title-row", children: [onBack && (
14
14
  /* 글자 없이 화살표만 둔다 — 제목 옆에서 '뒤로' 라고 적으면 제목과 경쟁한다.
15
15
  그만큼 과녁을 크게(48) 잡는다. 이름은 낭독기용으로 따로 준다. */
16
16
  _jsx(Button, { className: "page-title__back", size: 48, type: "dot", icon: "chevron-left", iconSize: "large", useBorder: false,