@ihabdevteam/core 0.239.0 → 0.241.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 +87 -0
- package/dist/components.cjs.js +177 -177
- package/dist/components.esm.css +2 -2
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +180 -180
- 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/WordTable.css +30 -1
- package/dist/packages/core/src/components/WordTable.js +1 -1
- package/dist/packages/core/src/components/barChartTypes.d.ts +9 -0
- package/dist/packages/core/src/i18n/locales/en.json +2 -0
- package/dist/packages/core/src/i18n/locales/ko.json +2 -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.241.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;
|
|
@@ -718,16 +718,38 @@
|
|
|
718
718
|
* 셋을 늘 보이게 두지 않는 이유는 따로 있다 — 표 아래가 동그라미로 가득 차
|
|
719
719
|
* 정작 매긴 표시가 안 보인다. */
|
|
720
720
|
.word-table-cell__score-slot {
|
|
721
|
-
|
|
721
|
+
/* **지우지 않고 감춘다**(말귀 요청 (53), 2026-10-02).
|
|
722
|
+
`display: none` 이던 때는 **초점을 못 받아 자판으로 매길 길이 아예 없었다**
|
|
723
|
+
(말귀 실측: 채점 창의 단추 36 개 중 자판으로 닿는 것 0 개. `.focus()` 를 걸어도
|
|
724
|
+
`activeElement` 가 안 바뀐다). 그러면 `:focus-within` 을 더해도 안 풀린다 —
|
|
725
|
+
초점을 못 받으니 켜질 일이 없다. 닭과 달걀이다.
|
|
726
|
+
**채점은 이 앱이 치료사에게 내미는 일 그 자체**이고, 그 가운데 걸음이 자판으로
|
|
727
|
+
막혀 있었다. 아래 `@media (hover: none)` 을 둔 까닭("얹기에 기대면 안 되는 자리")이
|
|
728
|
+
자판에도 그대로 선다.
|
|
729
|
+
잘라서 감추면 **초점은 받고 눈에는 안 보인다** — 초점이 가면 아래 `:focus-within`
|
|
730
|
+
이 되돌려 보이게 한다(안 보이는 자리에 초점이 서면 어디 있는지 모른다). */
|
|
731
|
+
position: absolute;
|
|
732
|
+
width: 1px;
|
|
733
|
+
height: 1px;
|
|
734
|
+
overflow: hidden;
|
|
735
|
+
clip-path: inset(50%);
|
|
736
|
+
white-space: nowrap;
|
|
722
737
|
}
|
|
723
738
|
|
|
724
739
|
/* 고른 것은 늘 보인다 — 표를 훑어 "어디를 어떻게 봤는지"가 한눈에 들어와야 한다.
|
|
725
740
|
마우스를 올리거나 포커스를 받으면 셋이 함께 나타나 고를 수 있다. */
|
|
726
741
|
.word-table-cell__score-slot--on,
|
|
727
742
|
.word-table-block__card:hover .word-table-cell__score-slot,
|
|
743
|
+
.word-table-block__card:focus-within .word-table-cell__score-slot,
|
|
728
744
|
.word-table-block__edit-cell:hover .word-table-cell__score-slot,
|
|
729
745
|
.word-table-block__edit-cell:focus-within .word-table-cell__score-slot,
|
|
730
746
|
.word-table-cell__score:focus-within .word-table-cell__score-slot {
|
|
747
|
+
/* 잘라 둔 것을 되돌린다 — 위 다섯 줄을 모두 거둬야 제자리에 선다 */
|
|
748
|
+
position: static;
|
|
749
|
+
width: auto;
|
|
750
|
+
height: auto;
|
|
751
|
+
overflow: visible;
|
|
752
|
+
clip-path: none;
|
|
731
753
|
display: inline-flex;
|
|
732
754
|
}
|
|
733
755
|
|
|
@@ -738,6 +760,11 @@
|
|
|
738
760
|
라고 적혀 있지만 매기는 것은 슬롯 단추뿐이라, 보이지 않으면 길이 없다. */
|
|
739
761
|
@media (hover: none) {
|
|
740
762
|
.word-table-cell__score-slot {
|
|
763
|
+
position: static;
|
|
764
|
+
width: auto;
|
|
765
|
+
height: auto;
|
|
766
|
+
overflow: visible;
|
|
767
|
+
clip-path: none;
|
|
741
768
|
display: inline-flex;
|
|
742
769
|
}
|
|
743
770
|
}
|
|
@@ -788,6 +815,8 @@
|
|
|
788
815
|
손가락 기기에서는 아예 늘 서 있다. 지난 채점을 그대로 펴 보이는 창에서는 누를 일이
|
|
789
816
|
없는데, 그 셋이 서면 **아직 안 매긴 표처럼** 보인다.
|
|
790
817
|
`--on`(고른 것)은 건드리지 않는다 — 그것이 이 표의 알맹이다. */
|
|
818
|
+
/* 읽기만 하는 표에서는 **지운다** — 자판으로 매길 일이 없으니 감출 까닭도 없고,
|
|
819
|
+
잘라서 두면 아무 일도 안 하는 정지점이 칸마다 둘씩 는다(㊹ 에서 걷은 그 자리다). */
|
|
791
820
|
.word-table--score-readonly .word-table-cell__score-slot:not(.word-table-cell__score-slot--on) {
|
|
792
821
|
display: none;
|
|
793
822
|
}
|
|
@@ -160,7 +160,7 @@ function WordCell({ label, subLabel, colloquialLabel, homonum, showWord = true,
|
|
|
160
160
|
obs.observe(el);
|
|
161
161
|
return () => obs.disconnect();
|
|
162
162
|
}, [refW, refH]);
|
|
163
|
-
return (_jsxs("div", { ref: outerRef, className: cx('word-table-cell', hasImage ? 'word-table-cell--with-image' : ''), "data-ref-w": refW, "data-ref-h": refH, children: [_jsxs("div", { className: "word-table-cell__inner", style: { transform: `scale(${scale})` }, children: [hasImage && (_jsxs("div", { className: "word-table-cell__image-wrap", children: [imgLoading && _jsx("div", { className: "word-table-cell__spinner" }), _jsx("img", { ref: imgRef, className: "word-table-cell__image", src: imageUrl, alt: label, draggable: false, style: { opacity: imgLoading ? 0 : 1 }, onLoad: handleImgLoad, onError: handleImgError })] })), showWord && _jsx("span", { className: "word-table-cell__word h0", children: label }), showPronunciation && pron && _jsxs("span", { className: "word-table-cell__sub h3", children: ["[", pron, "]"] }), showColloquial && colloquialLabel && _jsxs("span", { className: "word-table-cell__colloquial h5", children: ["[", colloquialLabel, "]"] })] }), _jsxs("div", { className: "word-table-cell__play-button", "data-audio-src": audioUrl, children: [_jsx("span", { "data-play-state": "play", style: { display: playing ? 'none' : undefined }, children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "dot", icon: "play", variant: "transparent-lightGray", useBorder: false, showTooltipInfo: t('common.play', '재생'), onClick: handlePlay }) }), _jsx("span", { "data-play-state": "pause", style: { display: playing ? undefined : 'none' }, children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "dot", icon: "pause", useBorder: false, selected: true, showTooltipInfo: t('common.pause', '멈춤'), onClick: handlePlay }) })] }), wordInfo && (_jsxs("div", { ref: infoRef, className: "word-table-cell__info-button", onPointerEnter: (e) => { if (!isTouchPointer(e))
|
|
163
|
+
return (_jsxs("div", { ref: outerRef, className: cx('word-table-cell', hasImage ? 'word-table-cell--with-image' : ''), "data-ref-w": refW, "data-ref-h": refH, children: [_jsxs("div", { className: "word-table-cell__inner", style: { transform: `scale(${scale})` }, children: [hasImage && (_jsxs("div", { className: "word-table-cell__image-wrap", children: [imgLoading && _jsx("div", { className: "word-table-cell__spinner" }), _jsx("img", { ref: imgRef, className: "word-table-cell__image", src: imageUrl, alt: label, draggable: false, style: { opacity: imgLoading ? 0 : 1 }, onLoad: handleImgLoad, onError: handleImgError })] })), showWord && _jsx("span", { className: "word-table-cell__word h0", children: label }), showPronunciation && pron && _jsxs("span", { className: "word-table-cell__sub h3", children: ["[", pron, "]"] }), showColloquial && colloquialLabel && _jsxs("span", { className: "word-table-cell__colloquial h5", children: ["[", colloquialLabel, "]"] })] }), _jsxs("div", { className: "word-table-cell__play-button", "data-audio-src": audioUrl, children: [_jsx("span", { "data-play-state": "play", style: { display: playing ? 'none' : undefined }, children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "dot", icon: "play", variant: "transparent-lightGray", useBorder: false, showTooltipInfo: t('common.play', '재생'), ariaLabel: label ? t('common.playFor', { name: label, defaultValue: `${label} 재생` }) : t('common.play', '재생'), onClick: handlePlay }) }), _jsx("span", { "data-play-state": "pause", style: { display: playing ? undefined : 'none' }, children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "dot", icon: "pause", useBorder: false, selected: true, showTooltipInfo: t('common.pause', '멈춤'), ariaLabel: label ? t('common.pauseFor', { name: label, defaultValue: `${label} 멈춤` }) : t('common.pause', '멈춤'), onClick: handlePlay }) })] }), wordInfo && (_jsxs("div", { ref: infoRef, className: "word-table-cell__info-button", onPointerEnter: (e) => { if (!isTouchPointer(e))
|
|
164
164
|
handleInfoEnter(); }, onPointerLeave: (e) => { if (!isTouchPointer(e))
|
|
165
165
|
handleInfoLeave(); },
|
|
166
166
|
// 이 div 자체는 포커스 대상이 아니지만 안의 Button이 포커스를 받으면
|
|
@@ -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과 동일하게 유지
|