@ihabdevteam/core 0.239.0 → 0.240.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/dist/components.cjs.js +176 -176
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +179 -179
- package/dist/ihabdevteam-core.d.ts +22 -3
- package/dist/packages/core/src/components/BarChart.d.ts +1 -1
- package/dist/packages/core/src/components/BarChart.js +68 -39
- package/dist/packages/core/src/components/ChartSection.d.ts +11 -1
- package/dist/packages/core/src/components/ChartSection.js +6 -4
- package/dist/packages/core/src/components/TableField.css +6 -1
- package/dist/packages/core/src/components/barChartTypes.d.ts +9 -0
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +1 -1
- package/package.json +1 -1
|
@@ -310,7 +310,7 @@ export declare type BadgeType = 'bar' | 'dot';
|
|
|
310
310
|
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
311
311
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
312
312
|
*/
|
|
313
|
-
export declare function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, headSegment, icon, disc, onClick, cardAriaLabel, cardTooltip, detailSize, barGap, baseline, className, inGrid, }: BarChartProps): JSX.Element;
|
|
313
|
+
export declare function BarChart({ focusableBars, size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, headSegment, icon, disc, onClick, cardAriaLabel, cardTooltip, detailSize, barGap, baseline, className, inGrid, }: BarChartProps): JSX.Element;
|
|
314
314
|
|
|
315
315
|
/** @public */
|
|
316
316
|
export declare interface BarChartControl {
|
|
@@ -347,6 +347,15 @@ export declare function BarChartControls({ control }: {
|
|
|
347
347
|
}): JSX.Element;
|
|
348
348
|
|
|
349
349
|
export declare interface BarChartProps {
|
|
350
|
+
/** 막대 칸을 **자판으로 짚을 수 있게** 한다(말귀 요청 (52), 2026-10-02).
|
|
351
|
+
*
|
|
352
|
+
* 켜면 칸마다 `tabIndex=0` 과 이름(`aria-label`)이 붙고, 초점을 받으면 얹었을 때와
|
|
353
|
+
* **같은 풍선**이 뜬다. 전에는 값이 얹어야 뜨는 풍선에만 있어 자판·낭독기로는 막대
|
|
354
|
+
* 하나하나의 수를 읽을 길이 **아예 없었다.**
|
|
355
|
+
*
|
|
356
|
+
* **기본은 거짓이다** — 막대가 서른이면 정지점이 서른 는다. 그 차림표가 화면의
|
|
357
|
+
* 본문인 자리(대시보드의 주 그래프 등)에서 켠다. 곁들이로 선 작은 그래프에는 안 켠다. */
|
|
358
|
+
focusableBars?: boolean;
|
|
350
359
|
/**
|
|
351
360
|
* 크기 — 'small'|'medium'|'large' (기본: 'medium'). 캔버스 크기뿐 아니라 정보량도 함께 조절한다.
|
|
352
361
|
* - small: 대시보드 작은 그리드용 블럭. 컨트롤(headRight)만 생략하고 total/legend는 medium과 동일하게 유지
|
|
@@ -1088,10 +1097,20 @@ export declare interface ChartPoint {
|
|
|
1088
1097
|
* 공유하도록 두 컴포넌트가 내부적으로 사용한다.
|
|
1089
1098
|
* @public
|
|
1090
1099
|
*/
|
|
1091
|
-
export declare function ChartSection({ title, titleClassName, icon, disc, headRight, headFloat, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, subRowAlign, className, inGrid, children }: ChartSectionProps): JSX.Element;
|
|
1100
|
+
export declare function ChartSection({ title, titleClassName, titleLevel, icon, disc, headRight, headFloat, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, subRowAlign, className, inGrid, children }: ChartSectionProps): JSX.Element;
|
|
1092
1101
|
|
|
1093
1102
|
/** @public */
|
|
1094
1103
|
export declare interface ChartSectionProps {
|
|
1104
|
+
/** 제목을 **머리글 몇 급**으로 그릴지(말귀 요청 (51), 2026-10-02).
|
|
1105
|
+
*
|
|
1106
|
+
* 전에는 제목이 `<div class="h3">` 였다 — `h3` 는 **글자 꼴**이지 머리글이 아니다.
|
|
1107
|
+
* 그래서 창 하나에 칸이 셋 서는데 **머리글이 0 개**였고, 낭독기로 건너뛸 자리가
|
|
1108
|
+
* 없었다(말귀 실측: 단어훈련 분석 창에서 h1~h6 · `role=heading` 둘 다 0).
|
|
1109
|
+
*
|
|
1110
|
+
* 기본 3 은 글자 꼴 기본(`titleClassName = 'h3'`)과 같은 급이다. 페이지 본문에 바로
|
|
1111
|
+
* 서는 칸이면 2 로 올린다 — 급을 건너뛰면 머리글로 훑는 사람에게 층이 빈 것처럼 읽힌다.
|
|
1112
|
+
* `0` 을 주면 전과 같이 `div` 로 그린다(머리글 급을 부르는 쪽이 못 정하는 자리용). */
|
|
1113
|
+
titleLevel?: 0 | 2 | 3 | 4 | 5 | 6;
|
|
1095
1114
|
/** 섹션 제목 */
|
|
1096
1115
|
title: ReactNode;
|
|
1097
1116
|
/** 제목 왼쪽 아이콘 — `Section` 의 것과 같은 자리·같은 색 */
|
|
@@ -1626,7 +1645,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1626
1645
|
*
|
|
1627
1646
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1628
1647
|
*/
|
|
1629
|
-
export declare const CORE_VERSION = "0.
|
|
1648
|
+
export declare const CORE_VERSION = "0.240.0";
|
|
1630
1649
|
|
|
1631
1650
|
/** @public */
|
|
1632
1651
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -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, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, headSegment, icon, disc, onClick, cardAriaLabel, cardTooltip, detailSize, barGap, baseline, className, inGrid, }: BarChartProps): import("react").JSX.Element;
|
|
16
|
+
export default function BarChart({ focusableBars, size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, headSegment, icon, disc, onClick, cardAriaLabel, cardTooltip, detailSize, barGap, baseline, className, inGrid, }: BarChartProps): import("react").JSX.Element;
|
|
@@ -22,7 +22,7 @@ function 미래칸(d, 옛표시 = true) {
|
|
|
22
22
|
/* `baseline` 을 쓰는 차트는 −1 이 진짜 값이라 옛 표시를 보지 않는다 — 부르는 쪽이 옛표시=false 로 끈다 */
|
|
23
23
|
return d.future === true || (옛표시 && d.a === -1);
|
|
24
24
|
}
|
|
25
|
-
function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, barGap = 1, height = 140, maxOverride, tightMax, seriesColors, seriesLabels, rateLabels, grouped = false, baseline }) {
|
|
25
|
+
function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, barGap = 1, height = 140, maxOverride, tightMax, seriesColors, seriesLabels, rateLabels, grouped = false, baseline, focusableBars = false }) {
|
|
26
26
|
// colorB 미지정 시 단일 계열(A) 모드
|
|
27
27
|
const single = !colorB;
|
|
28
28
|
const stacked = !!(seriesColors && seriesColors.length > 0);
|
|
@@ -73,6 +73,54 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, b
|
|
|
73
73
|
const 눈금자리 = (t) => (기준 != null ? (1 - (t - (기준 - padded)) / (2 * padded)) : (1 - t / padded));
|
|
74
74
|
const [tooltip, setTooltip] = useState(null);
|
|
75
75
|
const handleBarLeave = useCallback(() => setTooltip(null), []);
|
|
76
|
+
/* **한 칸이 말하는 것을 한 곳에서 짓는다**(말귀 요청 (52), 2026-10-02).
|
|
77
|
+
전에는 이 셈이 `onMouseEnter` 손잡이 안에만 있어, **얹는 사람만** 수를 읽을 수 있었다.
|
|
78
|
+
같은 글을 풍선·초점·`aria-label` 셋이 쓰도록 밖으로 뺀다 — 보는 말과 들리는 말이
|
|
79
|
+
갈리지 않는다. */
|
|
80
|
+
const 풍선내용 = useCallback((d, parts) => {
|
|
81
|
+
const title = [d.label, d.sublabel].filter(Boolean).join(' ');
|
|
82
|
+
let items;
|
|
83
|
+
if (stacked && grouped && d.rawPartsB) {
|
|
84
|
+
/* 계열별 비율 — 잰 것이 없는 계열은 0% 가 아니라 '-' 다(0% 는 "다 틀렸다" 로 읽힌다) */
|
|
85
|
+
items = (seriesLabels ?? []).map((lb, k) => {
|
|
86
|
+
const 분모 = d.rawPartsB[k] ?? 0;
|
|
87
|
+
return { text: 분모 > 0 ? `${lb}: ${parts[k] ?? 0}% (${d.rawParts?.[k] ?? 0}/${분모})` : `${lb}: -`, color: (seriesColors ?? [])[k] };
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else if (stacked && grouped) {
|
|
91
|
+
/* 자리가 고정이라 0 도 적는다 — 빈 자리가 무엇인지 알 수 있게 */
|
|
92
|
+
items = (seriesLabels ?? []).map((lb, k) => ({ text: `${lb}: ${부호붙이기(parts[k] ?? 0)}`, color: (seriesColors ?? [])[k] }));
|
|
93
|
+
}
|
|
94
|
+
else if (stacked) {
|
|
95
|
+
const 전부 = (seriesLabels ?? []).map((lb, k) => ({ text: `${lb}: ${parts[k] ?? 0}`, color: (seriesColors ?? [])[k] }));
|
|
96
|
+
const 있는 = 전부.filter((_, k) => (parts[k] ?? 0) > 0);
|
|
97
|
+
items = 있는.length ? 있는 : 전부;
|
|
98
|
+
}
|
|
99
|
+
else if (rateLabels && !single) {
|
|
100
|
+
items = [{ text: `${labelA} ${수(d.a)}%`, color: colorA }, { text: `${labelB} ${수(d.b)}%`, color: colorB }];
|
|
101
|
+
}
|
|
102
|
+
else if (rateLabels && d.rawB !== undefined) {
|
|
103
|
+
items = [{ text: `${rateLabels.total} ${d.rawB}` }, { text: `${rateLabels.done} ${d.rawA ?? 0}`, color: colorA }];
|
|
104
|
+
}
|
|
105
|
+
else if (single) {
|
|
106
|
+
items = [{ text: `${labelA}: ${수(d.a)}`, color: colorA }];
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
items = [{ text: `${labelA}: ${수(d.a)}`, color: colorA }, { text: `${labelB}: ${수(d.b)}`, color: colorB }];
|
|
110
|
+
}
|
|
111
|
+
return { title, items };
|
|
112
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
113
|
+
}, [stacked, grouped, seriesLabels, seriesColors, rateLabels, single, labelA, labelB, colorA, colorB, 기준]);
|
|
114
|
+
const 풍선열기 = useCallback((el, d, parts) => {
|
|
115
|
+
const r = el.getBoundingClientRect();
|
|
116
|
+
const { title, items } = 풍선내용(d, parts);
|
|
117
|
+
setTooltip({ rect: { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height }, title, items });
|
|
118
|
+
}, [풍선내용]);
|
|
119
|
+
/* 낭독기가 읽을 한 칸의 이름 — 풍선과 같은 글을 한 줄로 잇는다 */
|
|
120
|
+
const 막대이름 = useCallback((d, parts) => {
|
|
121
|
+
const { title, items } = 풍선내용(d, parts);
|
|
122
|
+
return [title, ...items.map(i => i.text)].filter(Boolean).join(', ');
|
|
123
|
+
}, [풍선내용]);
|
|
76
124
|
/* 기준선 모드에서는 오름·내림이 뜻이라 양수에도 + 를 붙인다(부호가 없으면 툴팁만 보고 방향을 알 수 없다) */
|
|
77
125
|
const 부호붙이기 = (v) => (기준 != null && v > 기준 ? `+${v}` : `${v}`);
|
|
78
126
|
/* 떠 있는 동안 **스크롤하거나 막대 밖을 누르면** 닫는다 — 터치에서는 톡이 호환 mouseenter 로 띄우고 mouseleave 는 다른 곳을
|
|
@@ -108,42 +156,23 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, b
|
|
|
108
156
|
const ha = isFuture ? 0 : 높이(d.a);
|
|
109
157
|
const hb = isFuture ? 0 : 높이(d.b);
|
|
110
158
|
const parts = (d.parts ?? []).map(수);
|
|
111
|
-
return (_jsx("div", { className: `manager-bar-chart-col${grouped && stacked ? ' manager-bar-chart-col--group' : ''}`,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
else if (stacked) {
|
|
129
|
-
const 전부 = (seriesLabels ?? []).map((lb, k) => ({ text: `${lb}: ${parts[k] ?? 0}`, color: (seriesColors ?? [])[k] }));
|
|
130
|
-
const 있는 = 전부.filter((_, k) => (parts[k] ?? 0) > 0);
|
|
131
|
-
items = 있는.length ? 있는 : 전부;
|
|
132
|
-
}
|
|
133
|
-
else if (rateLabels && !single) {
|
|
134
|
-
items = [{ text: `${labelA} ${수(d.a)}%`, color: colorA }, { text: `${labelB} ${수(d.b)}%`, color: colorB }];
|
|
135
|
-
}
|
|
136
|
-
else if (rateLabels && d.rawB !== undefined) {
|
|
137
|
-
items = [{ text: `${rateLabels.total} ${d.rawB}` }, { text: `${rateLabels.done} ${d.rawA ?? 0}`, color: colorA }];
|
|
138
|
-
}
|
|
139
|
-
else if (single) {
|
|
140
|
-
items = [{ text: `${labelA}: ${수(d.a)}`, color: colorA }];
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
items = [{ text: `${labelA}: ${수(d.a)}`, color: colorA }, { text: `${labelB}: ${수(d.b)}`, color: colorB }];
|
|
144
|
-
}
|
|
145
|
-
setTooltip({ rect: { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height }, title, items });
|
|
146
|
-
}, onMouseLeave: handleBarLeave, children: !isFuture && (stacked && grouped ? (
|
|
159
|
+
return (_jsx("div", { className: `manager-bar-chart-col${grouped && stacked ? ' manager-bar-chart-col--group' : ''}`,
|
|
160
|
+
/* **손가락은 그대로 둔다 — 걷으면 아이폰에서 풍선이 아예 안 뜬다.**
|
|
161
|
+
말귀가 "`onClick` 이 없는데 손가락이 뜬다"(요청 (52))고 짚은 것이 맞다.
|
|
162
|
+
그런데 **iOS 는 '누를 수 없는' 요소에 톡하면 호환 마우스 이벤트를 아예
|
|
163
|
+
안 보낸다** — `cursor: pointer` 가 그 판정에 든다(`test/
|
|
164
|
+
touchInteractions0915.test.tsx` 머리글에 그 차례가 적혀 있다).
|
|
165
|
+
걷으면 데스크톱의 거짓 손가락 하나를 없애는 대신 **손가락으로 쓰는
|
|
166
|
+
기기에서 수를 읽을 길이 통째로 사라진다.** 얹으면 값이 뜨니 아주 거짓도
|
|
167
|
+
아니다. 자판 쪽은 아래 `focusableBars` 가 따로 연다. */
|
|
168
|
+
style: { cursor: isFuture ? 'default' : 'pointer' }, tabIndex: focusableBars && !isFuture ? 0 : undefined, role: focusableBars && !isFuture ? 'img' : undefined, "aria-label": focusableBars && !isFuture ? 막대이름(d, parts) : undefined, onMouseEnter: isFuture ? undefined : (e) => 풍선열기(e.currentTarget, d, parts), onFocus: isFuture || !focusableBars ? undefined : (e) => 풍선열기(e.currentTarget, d, parts), onBlur: focusableBars ? handleBarLeave : undefined,
|
|
169
|
+
/* **수를 자판으로도 읽는다**(말귀 요청 (52), 2026-10-02). 전에는 값이
|
|
170
|
+
얹어야 뜨는 풍선에만 있어, 자판·낭독기로는 막대 하나하나의 수를 읽을
|
|
171
|
+
길이 **아예 없었다**. 이름은 풍선과 같은 글로 짓는다 — 보는 말과
|
|
172
|
+
들리는 말이 갈리지 않는다.
|
|
173
|
+
**기본은 꺼 둔다**: 막대가 서른이면 정지점이 서른 는다. 그 차림표가
|
|
174
|
+
화면의 본문인 자리에서만 켠다. */
|
|
175
|
+
onMouseLeave: handleBarLeave, children: !isFuture && (stacked && grouped ? (
|
|
147
176
|
/* 나란히 — 계열 k 는 늘 k 번째 자리. 0 이어도 자리를 두어 칸마다 같은 색이 같은 곳에 선다 */
|
|
148
177
|
(seriesColors ?? []).map((c, k) => {
|
|
149
178
|
const v = parts[k] ?? 0;
|
|
@@ -261,7 +290,7 @@ const BAR_SIZE_PRESET = {
|
|
|
261
290
|
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
262
291
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
263
292
|
*/
|
|
264
|
-
export default function BarChart({ size = 'medium', barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal = false, totalUnitKey, totalLabelKey, control: controlProp, hideControls = false, rate = false, rateAgg = 'dayMean', tightMax = false, avgMode = false, totalFormat, series, seriesMode = 'stack', loading = false, onDetail, headExtra, headSegment, icon, disc, onClick, cardAriaLabel, cardTooltip, detailSize = 32, barGap, baseline, className, inGrid = false, }) {
|
|
293
|
+
export default function BarChart({ focusableBars, size = 'medium', barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal = false, totalUnitKey, totalLabelKey, control: controlProp, hideControls = false, rate = false, rateAgg = 'dayMean', tightMax = false, avgMode = false, totalFormat, series, seriesMode = 'stack', loading = false, onDetail, headExtra, headSegment, icon, disc, onClick, cardAriaLabel, cardTooltip, detailSize = 32, barGap, baseline, className, inGrid = false, }) {
|
|
265
294
|
const { t, i18n } = useTranslation();
|
|
266
295
|
const sizePreset = BAR_SIZE_PRESET[size];
|
|
267
296
|
// 외부 control 미지정 시 내부 컨트롤 사용 (단독 차트 하위 호환)
|
|
@@ -412,5 +441,5 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
|
|
|
412
441
|
→ 막대 156px 로 카드마다 30px, 섹션 372 → 342px.
|
|
413
442
|
그래서 **그려질 것과 같은 뼈대**(칸 자리 + 축 한 줄)를 세우고 그 안에 스피너를 둔다.
|
|
414
443
|
키는 막대 자리(`sizePreset.height`)에서 오므로 크기 보기(size)가 달라도 따라온다. */
|
|
415
|
-
_jsx("div", { className: "manager-bar-chart-wrap manager-bar-chart-loading", "aria-busy": "true", children: _jsx("div", { className: "manager-bar-chart-inner", children: _jsxs("div", { className: "manager-bar-chart-area manager-bar-chart-area--loading", children: [_jsx("div", { className: "manager-bar-chart-plot manager-bar-chart-plot--loading", style: { height: sizePreset.height }, children: _jsx(Loader, { size: 32 }) }), _jsx("div", { className: "manager-bar-chart-x-axis", "aria-hidden": "true", children: _jsxs("span", { className: "manager-bar-chart-x-axis__label", children: [_jsx("span", { className: "caption-1 manager-bar-chart-x-axis__main", children: "\u00A0" }), bars.some(b => !!b.sublabel) && _jsx("span", { className: "caption-1", children: "\u00A0" })] }) })] }) }) })) : (_jsx(BarChartCanvas, { data: bars, colorA: colorA, colorB: labelBKey ? colorB : undefined, labelA: t(labelAKey), labelB: labelBKey ? t(labelBKey) : undefined, barWidth: barWidth ?? sizePreset.barWidth, barGap: barGap, baseline: baseline, height: sizePreset.height, maxOverride: rate ? 100 : undefined, tightMax: tightMax, seriesColors: series?.map(s => s.color), seriesLabels: series?.map(s => s.label), rateLabels: rate ? { total: t('trainingStatus.today.tooltipTotal', '전체'), done: t('trainingStatus.today.tooltipDone', '완료') } : undefined, grouped: 나란히 })) }));
|
|
444
|
+
_jsx("div", { className: "manager-bar-chart-wrap manager-bar-chart-loading", "aria-busy": "true", children: _jsx("div", { className: "manager-bar-chart-inner", children: _jsxs("div", { className: "manager-bar-chart-area manager-bar-chart-area--loading", children: [_jsx("div", { className: "manager-bar-chart-plot manager-bar-chart-plot--loading", style: { height: sizePreset.height }, children: _jsx(Loader, { size: 32 }) }), _jsx("div", { className: "manager-bar-chart-x-axis", "aria-hidden": "true", children: _jsxs("span", { className: "manager-bar-chart-x-axis__label", children: [_jsx("span", { className: "caption-1 manager-bar-chart-x-axis__main", children: "\u00A0" }), bars.some(b => !!b.sublabel) && _jsx("span", { className: "caption-1", children: "\u00A0" })] }) })] }) }) })) : (_jsx(BarChartCanvas, { focusableBars: focusableBars, data: bars, colorA: colorA, colorB: labelBKey ? colorB : undefined, labelA: t(labelAKey), labelB: labelBKey ? t(labelBKey) : undefined, barWidth: barWidth ?? sizePreset.barWidth, barGap: barGap, baseline: baseline, height: sizePreset.height, maxOverride: rate ? 100 : undefined, tightMax: tightMax, seriesColors: series?.map(s => s.color), seriesLabels: series?.map(s => s.label), rateLabels: rate ? { total: t('trainingStatus.today.tooltipTotal', '전체'), done: t('trainingStatus.today.tooltipDone', '완료') } : undefined, grouped: 나란히 })) }));
|
|
416
445
|
}
|
|
@@ -37,6 +37,16 @@ export interface ChartHeadSegment {
|
|
|
37
37
|
}
|
|
38
38
|
/** @public */
|
|
39
39
|
export interface ChartSectionProps {
|
|
40
|
+
/** 제목을 **머리글 몇 급**으로 그릴지(말귀 요청 (51), 2026-10-02).
|
|
41
|
+
*
|
|
42
|
+
* 전에는 제목이 `<div class="h3">` 였다 — `h3` 는 **글자 꼴**이지 머리글이 아니다.
|
|
43
|
+
* 그래서 창 하나에 칸이 셋 서는데 **머리글이 0 개**였고, 낭독기로 건너뛸 자리가
|
|
44
|
+
* 없었다(말귀 실측: 단어훈련 분석 창에서 h1~h6 · `role=heading` 둘 다 0).
|
|
45
|
+
*
|
|
46
|
+
* 기본 3 은 글자 꼴 기본(`titleClassName = 'h3'`)과 같은 급이다. 페이지 본문에 바로
|
|
47
|
+
* 서는 칸이면 2 로 올린다 — 급을 건너뛰면 머리글로 훑는 사람에게 층이 빈 것처럼 읽힌다.
|
|
48
|
+
* `0` 을 주면 전과 같이 `div` 로 그린다(머리글 급을 부르는 쪽이 못 정하는 자리용). */
|
|
49
|
+
titleLevel?: 0 | 2 | 3 | 4 | 5 | 6;
|
|
40
50
|
/** 섹션 제목 */
|
|
41
51
|
title: ReactNode;
|
|
42
52
|
/** 제목 왼쪽 아이콘 — `Section` 의 것과 같은 자리·같은 색 */
|
|
@@ -116,4 +126,4 @@ export interface ChartSectionProps {
|
|
|
116
126
|
* 공유하도록 두 컴포넌트가 내부적으로 사용한다.
|
|
117
127
|
* @public
|
|
118
128
|
*/
|
|
119
|
-
export default function ChartSection({ title, titleClassName, icon, disc, headRight, headFloat, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, subRowAlign, className, inGrid, children }: ChartSectionProps): import("react").JSX.Element;
|
|
129
|
+
export default function ChartSection({ title, titleClassName, titleLevel, icon, disc, headRight, headFloat, headSegment, headMenuItems, collapseBelow, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, subRowAlign, className, inGrid, children }: ChartSectionProps): import("react").JSX.Element;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import './ChartSection.css';
|
|
4
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import Icon from './Icon';
|
|
7
7
|
import SectionHeadFloat from './SectionHeadFloat';
|
|
@@ -29,7 +29,7 @@ export function ChartTotal({ label, children }) {
|
|
|
29
29
|
* 공유하도록 두 컴포넌트가 내부적으로 사용한다.
|
|
30
30
|
* @public
|
|
31
31
|
*/
|
|
32
|
-
export default function ChartSection({ title, titleClassName = 'h3', icon, disc, headRight, headFloat, headSegment, headMenuItems, collapseBelow = 420, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, subRowAlign = 'start', className, inGrid = false, children }) {
|
|
32
|
+
export default function ChartSection({ title, titleClassName = 'h3', titleLevel = 3, icon, disc, headRight, headFloat, headSegment, headMenuItems, collapseBelow = 420, onClick, ariaLabel, tooltip, totalNum, totalUnit, totalLabel, subRow, subRowAlign = 'start', className, inGrid = false, children }) {
|
|
33
33
|
const { t } = useTranslation();
|
|
34
34
|
const secRef = useRef(null);
|
|
35
35
|
/* 설명 툴팁 — StatChart 와 같은 결(카드 사각형 기준, 포인터가 나가거나 초점이 빠지면 닫힘).
|
|
@@ -107,12 +107,14 @@ export default function ChartSection({ title, titleClassName = 'h3', icon, disc,
|
|
|
107
107
|
const 수줄 = (totalNum != null || totalLabel) ? (_jsxs(ChartTotal, { label: totalLabel, children: [totalNum != null && _jsx("span", { className: "barchart-total__num h0", children: totalNum }), totalUnit && _jsx("span", { className: "barchart-total__unit h5", children: totalUnit })] })) : null;
|
|
108
108
|
/* 머리 오른쪽에는 **고르개와 `headRight` 만** 남는다 — 고르개가 먼저, 상세보기 단추가 오른쪽 끝(사용자 지시 2026-09-17) */
|
|
109
109
|
const 머리오른쪽 = (오른쪽 || headSegment) ? (_jsxs("div", { className: "manager-section__head-right", children: [headSegment && (접힘 ? (_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "ellipsis", ariaLabel: headSegment.ariaLabel ?? t('common.more', '더보기'), showTooltipInfo: headSegment.ariaLabel ?? t('common.more', '더보기'), "aria-haspopup": "menu", "aria-expanded": !!메뉴자리, onClick: 메뉴열기 })) : (_jsx(SegmentedControl, { size: 32, ariaLabel: headSegment.ariaLabel, options: headSegment.options.map(o => ({ key: o.key, label: o.label, disabled: o.disabled })), value: headSegment.value, onChange: headSegment.onChange }))), 오른쪽] })) : null;
|
|
110
|
-
|
|
110
|
+
const 제목id = useId();
|
|
111
|
+
const 제목태그 = titleLevel ? `h${titleLevel}` : 'div';
|
|
112
|
+
return (_jsxs("section", { ref: secRef, className: ['manager-section', inGrid && 'manager-section--grid', onClick && 'manager-section--clickable', className].filter(Boolean).join(' '), onClick: onClick ? 카드누름 : undefined, onKeyDown: onClick ? 카드키 : undefined, onPointerEnter: tooltip ? 설명보이기 : undefined, onPointerLeave: tooltip ? 설명닫기 : undefined, onFocus: tooltip ? 설명보이기 : undefined, onBlur: tooltip ? 설명닫기 : undefined, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, "aria-label": onClick ? (ariaLabel ?? (typeof title === 'string' ? title : undefined)) : undefined, "aria-labelledby": !onClick && title != null ? 제목id : undefined, children: [tooltip && 설명자리 && (_jsx(TooltipInfo, { anchorRect: {
|
|
111
113
|
left: 설명자리.left,
|
|
112
114
|
top: 설명자리.top + 21,
|
|
113
115
|
right: 설명자리.right,
|
|
114
116
|
bottom: 설명자리.bottom,
|
|
115
117
|
width: 설명자리.width,
|
|
116
118
|
height: 설명자리.height,
|
|
117
|
-
}, text: tooltip })), headFloat && _jsx(SectionHeadFloat, { children: headFloat }), _jsxs("div", { className: "manager-section__head", children: [_jsxs("div", { className: "manager-section__title-row", children: [icon && _jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "manager-section__icon" }), _jsx(
|
|
119
|
+
}, text: tooltip })), headFloat && _jsx(SectionHeadFloat, { children: headFloat }), _jsxs("div", { className: "manager-section__head", children: [_jsxs("div", { className: "manager-section__title-row", children: [icon && _jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "manager-section__icon" }), _jsx(제목태그, { id: 제목id, className: ['manager-section__title', titleClassName].filter(Boolean).join(' '), children: title })] }), 머리오른쪽, disc && _jsx("div", { className: "manager-section__disc body-3", children: disc })] }), (수줄 || subRow) && (_jsxs("div", { className: ['barchart-sub-row', subRowAlign !== 'start' && `barchart-sub-row--${subRowAlign}`].filter(Boolean).join(' '), children: [수줄, subRow] })), children, 메뉴자리 && (_jsx(Tooltip, { anchorRect: 메뉴자리, onClose: 메뉴닫기, ariaLabel: headSegment?.ariaLabel ?? t('tooltip.menu', '메뉴'), children: _jsx(MenuTooltip, { columns: [{ items: 메뉴항목 }], onClose: 메뉴닫기, size: "small" }) }))] }));
|
|
118
120
|
}
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
padding: 0 16px;
|
|
6
6
|
text-align: left;
|
|
7
7
|
color: var(--color-gray);
|
|
8
|
-
|
|
8
|
+
/* **손가락은 누를 수 있는 머리에만**(말귀가 짚음, 2026-10-02). 전에는 `.table-field`
|
|
9
|
+
전부에 걸려 **정렬 못 하는 칸까지** 손가락을 띄웠다(말귀 보고서 목록 머리 여섯).
|
|
10
|
+
누르는 것은 그 안의 진짜 `<button>` 이고, 그 단추가 서는 머리에만 띄운다.
|
|
11
|
+
여기는 `BarChart` 의 막대 칸과 다르다 — 거기는 손가락을 걷으면 iOS 가 호환 마우스
|
|
12
|
+
이벤트를 안 보내 풍선이 통째로 죽는다. 이 머리는 얹어서 얻을 것이 없어 그 셈이 없다. */
|
|
9
13
|
white-space: nowrap;
|
|
10
14
|
user-select: none;
|
|
11
15
|
/* 머리행과 본문은 별개의 grid라 컬럼 폭이 저마다 계산된다. .table-cell 에는 min-width:0
|
|
@@ -29,6 +33,7 @@
|
|
|
29
33
|
높이도 물려받게 `align-self: stretch` — kscan 이 `.table-field { height: 38px }`
|
|
30
34
|
로 줄여 둔 인쇄용 표에서 단추가 제 높이로 부풀지 않게 한다. */
|
|
31
35
|
.table-field__button {
|
|
36
|
+
cursor: pointer;
|
|
32
37
|
flex: 1 1 auto;
|
|
33
38
|
min-width: 0;
|
|
34
39
|
align-self: stretch;
|
|
@@ -32,6 +32,15 @@ export interface BarChartControl {
|
|
|
32
32
|
/** 시맨틱 사이즈 — 차트 캔버스 높이·막대 너비 프리셋 */
|
|
33
33
|
export type BarChartSize = 'small' | 'medium' | 'large';
|
|
34
34
|
export interface BarChartProps {
|
|
35
|
+
/** 막대 칸을 **자판으로 짚을 수 있게** 한다(말귀 요청 (52), 2026-10-02).
|
|
36
|
+
*
|
|
37
|
+
* 켜면 칸마다 `tabIndex=0` 과 이름(`aria-label`)이 붙고, 초점을 받으면 얹었을 때와
|
|
38
|
+
* **같은 풍선**이 뜬다. 전에는 값이 얹어야 뜨는 풍선에만 있어 자판·낭독기로는 막대
|
|
39
|
+
* 하나하나의 수를 읽을 길이 **아예 없었다.**
|
|
40
|
+
*
|
|
41
|
+
* **기본은 거짓이다** — 막대가 서른이면 정지점이 서른 는다. 그 차림표가 화면의
|
|
42
|
+
* 본문인 자리(대시보드의 주 그래프 등)에서 켠다. 곁들이로 선 작은 그래프에는 안 켠다. */
|
|
43
|
+
focusableBars?: boolean;
|
|
35
44
|
/**
|
|
36
45
|
* 크기 — 'small'|'medium'|'large' (기본: 'medium'). 캔버스 크기뿐 아니라 정보량도 함께 조절한다.
|
|
37
46
|
* - small: 대시보드 작은 그리드용 블럭. 컨트롤(headRight)만 생략하고 total/legend는 medium과 동일하게 유지
|