@ihabdevteam/core 0.161.0 → 0.163.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 +22 -0
- package/dist/components.cjs.js +185 -170
- 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 +50 -35
- package/dist/ihabdevteam-core.d.ts +9 -2
- package/dist/packages/core/src/components/BarChart.css +21 -0
- package/dist/packages/core/src/components/BarChart.d.ts +1 -1
- package/dist/packages/core/src/components/BarChart.js +50 -13
- package/dist/packages/core/src/components/ChartSection.css +4 -2
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.css +7 -0
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.d.ts +3 -0
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +11 -11
- package/dist/packages/core/src/components/barChartTypes.d.ts +4 -0
- package/dist/packages/core/src/utils/lucideIconSet.js +8 -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
|
@@ -309,7 +309,7 @@ export declare type BadgeType = 'bar' | 'dot';
|
|
|
309
309
|
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
310
310
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
311
311
|
*/
|
|
312
|
-
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, detailSize, barGap, className, inGrid, }: BarChartProps): JSX.Element;
|
|
312
|
+
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, detailSize, barGap, baseline, className, inGrid, }: BarChartProps): JSX.Element;
|
|
313
313
|
|
|
314
314
|
/** @public */
|
|
315
315
|
export declare interface BarChartControl {
|
|
@@ -410,6 +410,10 @@ export declare interface BarChartProps {
|
|
|
410
410
|
* 막대 수가 늘면 굵기는 `barWidth` 를 위 한계로 두고 줄어든다(계열 수만큼 센다).
|
|
411
411
|
*/
|
|
412
412
|
seriesMode?: 'stack' | 'group';
|
|
413
|
+
/** 0 선을 캔버스 가운데 두고 **음수는 아래로** 그린다(예: 변동률 %p). 주면 축은 위아래 대칭(±max|값|)이 되고,
|
|
414
|
+
* 눈금·격자선·이름표가 음수 구간까지 따라가며, 툴팁은 양수에 `+` 를 붙여 방향을 드러낸다.
|
|
415
|
+
* `series` + `seriesMode='group'` 과 함께 쓰면 갈래마다 0 선 위·아래로 자란다. 지금은 `0` 만 쓴다. */
|
|
416
|
+
baseline?: number;
|
|
413
417
|
/** 나란히 선 막대 사이 간격 px(기본 1). `seriesMode='group'` 에서만 쓰인다 — 한 칸에 서는 막대가 늘수록
|
|
414
418
|
* `barWidth` 와 함께 좁혀 묶음이 한 덩어리로 읽히게 한다(말귀: 5개면 굵기 6·간격 2 … 2개면 9·5). */
|
|
415
419
|
barGap?: number;
|
|
@@ -1496,7 +1500,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1496
1500
|
*
|
|
1497
1501
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1498
1502
|
*/
|
|
1499
|
-
export declare const CORE_VERSION = "0.
|
|
1503
|
+
export declare const CORE_VERSION = "0.163.0";
|
|
1500
1504
|
|
|
1501
1505
|
/** @public */
|
|
1502
1506
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -7019,6 +7023,9 @@ export declare type TooltipMenuColumnItem = {
|
|
|
7019
7023
|
disabled?: boolean;
|
|
7020
7024
|
/** 비활성 사유 등 hover 설명(title 속성). */
|
|
7021
7025
|
tooltip?: string;
|
|
7026
|
+
/** 이 항목 **앞** 에 가로 구분선을 그린다 — 성격이 다른 묶음을 가를 때(보기 고르개 항목들 ↔ 창을 여는 항목).
|
|
7027
|
+
* 선은 그리기만 할 뿐 항목이 아니라, 키보드 차례·낭독기 목록에 끼지 않는다(`role="separator"`). 첫 항목에 주면 무시한다. */
|
|
7028
|
+
separatorBefore?: boolean;
|
|
7022
7029
|
/** 우측 원형 배지 숫자(알림 개수 등). 라벨에 "(5)"를 붙이는 대신 SideNav와 같은
|
|
7023
7030
|
* Badge type="dot"으로 표시한다 — 두 메뉴가 같은 항목을 다르게 보여주지 않도록. */
|
|
7024
7031
|
badgeCount?: number;
|
|
@@ -151,6 +151,27 @@
|
|
|
151
151
|
/* 나란히(seriesMode 'group') — 한 칸에 계열이 네다섯 선다. **칸 사이가 계열 사이보다 늘 넓어야**
|
|
152
152
|
어느 막대가 어느 주·날인지 읽힌다. 계열 사이는 1px 로 붙이고, 칸 양옆에 4px 를 둬 칸 사이가
|
|
153
153
|
적어도 8px 이 되게 한다(막대가 줄어 칸이 꽉 차도 무리가 붙지 않는다). */
|
|
154
|
+
/* 0 선 기준 막대 — 반쪽 둘(위·아래)을 쌓아 둔 자리. 나란히 선 칸의 폭 나눔은 그대로 두고, 0 선에서 위나 아래로 자란다 */
|
|
155
|
+
.manager-bar-chart-slot {
|
|
156
|
+
flex: 0 1 var(--bar-width);
|
|
157
|
+
height: 100%;
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
min-width: 0;
|
|
161
|
+
}
|
|
162
|
+
.manager-bar-chart-slot__half {
|
|
163
|
+
height: 50%;
|
|
164
|
+
display: flex;
|
|
165
|
+
justify-content: center;
|
|
166
|
+
}
|
|
167
|
+
.manager-bar-chart-slot__half--up { align-items: flex-end; }
|
|
168
|
+
.manager-bar-chart-slot__half--down { align-items: flex-start; }
|
|
169
|
+
.manager-bar-chart-bar--full { flex: 1 1 auto; width: 100%; }
|
|
170
|
+
/* 아래로 자라는 막대는 아래쪽 모서리를 둥글린다 */
|
|
171
|
+
.manager-bar-chart-bar--down { border-radius: 0 0 6px 6px; }
|
|
172
|
+
/* 0 선은 다른 격자선보다 또렷하게 */
|
|
173
|
+
.manager-bar-chart-gridline--baseline { opacity: 1; }
|
|
174
|
+
|
|
154
175
|
.manager-bar-chart-col--group {
|
|
155
176
|
/* 기본 1px — barGap prop 으로 칸마다 바꾼다 */
|
|
156
177
|
gap: var(--bar-group-gap, 1px);
|
|
@@ -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, detailSize, barGap, className, inGrid, }: 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, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, detailSize, barGap, baseline, className, inGrid, }: BarChartProps): import("react").JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import './BarChart.css';
|
|
4
4
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
@@ -16,7 +16,7 @@ import { useBarChartControl } from './useBarChartControl';
|
|
|
16
16
|
// ─── 막대 렌더링 원시 컴포넌트 ───────────────────────────────────
|
|
17
17
|
/** 막대 값 — 수가 아니면(undefined·NaN·Infinity) 0 */
|
|
18
18
|
const 수 = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
19
|
-
function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, barGap = 1, height = 140, maxOverride, tightMax, seriesColors, seriesLabels, rateLabels, grouped = false }) {
|
|
19
|
+
function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, barGap = 1, height = 140, maxOverride, tightMax, seriesColors, seriesLabels, rateLabels, grouped = false, baseline }) {
|
|
20
20
|
// colorB 미지정 시 단일 계열(A) 모드
|
|
21
21
|
const single = !colorB;
|
|
22
22
|
const stacked = !!(seriesColors && seriesColors.length > 0);
|
|
@@ -28,7 +28,18 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, b
|
|
|
28
28
|
? Math.max(0, ...(d.parts ?? []).map(수))
|
|
29
29
|
: (d.parts ?? []).reduce((s, v) => s + Math.max(0, 수(v)), 0)))
|
|
30
30
|
: Math.max(0, ...data.filter(d => d.a !== -1).flatMap(d => single ? [수(d.a)] : [수(d.a), 수(d.b)])), [data, single, stacked, grouped]);
|
|
31
|
-
|
|
31
|
+
/* 기준선 모드: 0 선에서 얼마나 떨어졌는지(|값 − 기준|)가 축을 정한다 — 위아래가 대칭이라야 같은 크기의 오름·내림이 같은 길이로 읽힌다 */
|
|
32
|
+
const 기준 = baseline;
|
|
33
|
+
const 대칭최대 = useMemo(() => {
|
|
34
|
+
if (기준 == null)
|
|
35
|
+
return 0;
|
|
36
|
+
const 값들 = data.filter(d => d.a !== -1).flatMap(d => stacked ? (d.parts ?? []).map(수) : (single ? [수(d.a)] : [수(d.a), 수(d.b)]));
|
|
37
|
+
return Math.max(0, ...값들.map(v => Math.abs(v - 기준)));
|
|
38
|
+
}, [data, 기준, stacked, single]);
|
|
39
|
+
const padded = useMemo(() => {
|
|
40
|
+
const m = 기준 != null ? 대칭최대 : maxVal;
|
|
41
|
+
return maxOverride ?? (tightMax ? (m > 0 ? m : 10) : (Math.ceil(m / 10) * 10 || 10));
|
|
42
|
+
}, [maxVal, 대칭최대, 기준, maxOverride, tightMax]);
|
|
32
43
|
const h = height;
|
|
33
44
|
// 막대 너비 — 여백과 무관하게 막대 '개수'(컬럼 × 계열)가 늘수록 점진 축소(최대 barWidth)
|
|
34
45
|
const barsPerCol = stacked ? (grouped ? Math.max(1, seriesColors.length) : 1) : (single ? 1 : 2);
|
|
@@ -44,10 +55,19 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, b
|
|
|
44
55
|
const 묶음간격 = { '--bar-group-gap': `${Math.max(0, barGap)}px` };
|
|
45
56
|
const ticks = useMemo(() => {
|
|
46
57
|
const mid = Math.round(padded / 2);
|
|
58
|
+
if (기준 != null) {
|
|
59
|
+
/* 위 → 0 → 아래. 맨 아래 값은 x축 자리라 아래 렌더에서 뺀다(기존 규칙과 같다) */
|
|
60
|
+
const 위 = [기준 + padded, ...(mid !== padded ? [기준 + mid] : []), 기준];
|
|
61
|
+
return [...위, ...(mid !== padded ? [기준 - mid] : []), 기준 - padded];
|
|
62
|
+
}
|
|
47
63
|
return mid !== padded ? [padded, mid, 0] : [padded, 0];
|
|
48
|
-
}, [padded]);
|
|
64
|
+
}, [padded, 기준]);
|
|
65
|
+
/** 값 → 판 위에서의 높이 비율(0~1) 자리 — 기준선 모드는 0 선이 한가운데 */
|
|
66
|
+
const 눈금자리 = (t) => (기준 != null ? (1 - (t - (기준 - padded)) / (2 * padded)) : (1 - t / padded));
|
|
49
67
|
const [tooltip, setTooltip] = useState(null);
|
|
50
68
|
const handleBarLeave = useCallback(() => setTooltip(null), []);
|
|
69
|
+
/* 기준선 모드에서는 오름·내림이 뜻이라 양수에도 + 를 붙인다(부호가 없으면 툴팁만 보고 방향을 알 수 없다) */
|
|
70
|
+
const 부호붙이기 = (v) => (기준 != null && v > 기준 ? `+${v}` : `${v}`);
|
|
51
71
|
/* 떠 있는 동안 **스크롤하거나 막대 밖을 누르면** 닫는다 — 터치에서는 톡이 호환 mouseenter 로 띄우고 mouseleave 는 다른 곳을
|
|
52
72
|
눌러야 와서, 스크롤해도 제자리에 떠 있었고 누를 수 없는 곳을 톡해도 안 닫혔다(말귀 훈련 현황·매니저 화면, 태블릿). */
|
|
53
73
|
const 떠있음 = tooltip != null;
|
|
@@ -70,10 +90,14 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, b
|
|
|
70
90
|
}, [떠있음]);
|
|
71
91
|
return (_jsxs("div", { className: "manager-bar-chart-wrap", style: 묶음간격, 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) => (
|
|
72
92
|
/* 눈금선은 **적힌 값의 높이**에 둔다 — 가운데 값을 반올림(최대 3 → '2')하면서 자리는 절반(1.5)에 두어 선과 숫자가 어긋났다 */
|
|
73
|
-
_jsx("div", { className:
|
|
93
|
+
_jsx("div", { className: `manager-bar-chart-gridline${기준 != null && t === 기준 ? ' manager-bar-chart-gridline--baseline' : ''}`, style: { top: 눈금자리(t) * h }, children: _jsx("span", { className: "manager-bar-chart-gridline__label caption-1", children: t }) }, i))), data.map((d, i) => {
|
|
74
94
|
const isFuture = d.a === -1;
|
|
75
95
|
/* 판 높이를 넘지 않는다 — maxOverride(진행률 100)보다 큰 값이 와도 막대가 눈금 위로 삐져나가지 않게 */
|
|
76
|
-
|
|
96
|
+
/* 기준선 모드는 반쪽(h/2)을 쓴다 — 0 선에서 위나 아래로 자란다 */
|
|
97
|
+
const 반 = h / 2;
|
|
98
|
+
const 높이 = (v) => (기준 != null
|
|
99
|
+
? Math.min(반, (Math.abs(수(v) - 기준) / padded) * 반)
|
|
100
|
+
: Math.min(h, Math.max(0, (수(v) / padded) * h)));
|
|
77
101
|
const ha = isFuture ? 0 : 높이(d.a);
|
|
78
102
|
const hb = isFuture ? 0 : 높이(d.b);
|
|
79
103
|
const parts = (d.parts ?? []).map(수);
|
|
@@ -92,7 +116,7 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, b
|
|
|
92
116
|
}
|
|
93
117
|
else if (stacked && grouped) {
|
|
94
118
|
/* 자리가 고정이라 0 도 적는다 — 빈 자리가 무엇인지 알 수 있게 */
|
|
95
|
-
items = (seriesLabels ?? []).map((lb, k) => ({ text: `${lb}: ${parts[k] ?? 0}`, color: (seriesColors ?? [])[k] }));
|
|
119
|
+
items = (seriesLabels ?? []).map((lb, k) => ({ text: `${lb}: ${부호붙이기(parts[k] ?? 0)}`, color: (seriesColors ?? [])[k] }));
|
|
96
120
|
}
|
|
97
121
|
else if (stacked) {
|
|
98
122
|
const 전부 = (seriesLabels ?? []).map((lb, k) => ({ text: `${lb}: ${parts[k] ?? 0}`, color: (seriesColors ?? [])[k] }));
|
|
@@ -114,7 +138,14 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, b
|
|
|
114
138
|
setTooltip({ rect: { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height }, title, items });
|
|
115
139
|
}, onMouseLeave: handleBarLeave, children: !isFuture && (stacked && grouped ? (
|
|
116
140
|
/* 나란히 — 계열 k 는 늘 k 번째 자리. 0 이어도 자리를 두어 칸마다 같은 색이 같은 곳에 선다 */
|
|
117
|
-
(seriesColors ?? []).map((c, k) =>
|
|
141
|
+
(seriesColors ?? []).map((c, k) => {
|
|
142
|
+
const v = parts[k] ?? 0;
|
|
143
|
+
if (기준 == null)
|
|
144
|
+
return _jsx("div", { className: "manager-bar-chart-bar", style: { ...막대굵기, height: 높이(v), background: c } }, k);
|
|
145
|
+
/* 0 선 위·아래 — 반쪽 둘을 쌓아 둔 자리(slot)에 그려, 나란히 선 칸의 폭 나눔은 그대로 둔다 */
|
|
146
|
+
const 아래 = 수(v) < 기준;
|
|
147
|
+
return (_jsxs("div", { className: "manager-bar-chart-slot", style: 막대굵기, children: [_jsx("div", { className: "manager-bar-chart-slot__half manager-bar-chart-slot__half--up", children: !아래 && _jsx("div", { className: "manager-bar-chart-bar manager-bar-chart-bar--full", style: { height: 높이(v), background: c } }) }), _jsx("div", { className: "manager-bar-chart-slot__half manager-bar-chart-slot__half--down", children: 아래 && _jsx("div", { className: "manager-bar-chart-bar manager-bar-chart-bar--full manager-bar-chart-bar--down", style: { height: 높이(v), background: c } }) })] }, k));
|
|
148
|
+
})) : stacked ? (
|
|
118
149
|
// 계열별 stacked — 아래부터 위로 쌓고, 맨 위 segment만 상단 라운드
|
|
119
150
|
_jsx("div", { className: "manager-bar-chart-stack", style: 막대굵기, children: (() => {
|
|
120
151
|
const topK = parts.reduce((acc, v, k) => (v > 0 ? k : acc), -1); // 맨 위(최상단) 세그먼트
|
|
@@ -126,7 +157,7 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, b
|
|
|
126
157
|
남은 -= 조각;
|
|
127
158
|
return _jsx("div", { className: "manager-bar-chart-stack-segment", style: { height: 조각, background: (seriesColors ?? [])[k], borderRadius: k === topK ? '6px 6px 0 0' : undefined } }, k);
|
|
128
159
|
});
|
|
129
|
-
})() })) : (_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));
|
|
160
|
+
})() })) : (기준 != null ? (_jsx(_Fragment, { children: [{ v: 수(d.a), c: colorA }, ...(single ? [] : [{ v: 수(d.b), c: colorB }])].map(({ v, c }, k) => (_jsxs("div", { className: "manager-bar-chart-slot", style: 막대굵기, children: [_jsx("div", { className: "manager-bar-chart-slot__half manager-bar-chart-slot__half--up", children: v >= 기준 && _jsx("div", { className: "manager-bar-chart-bar manager-bar-chart-bar--full", style: { height: 높이(v), background: c } }) }), _jsx("div", { className: "manager-bar-chart-slot__half manager-bar-chart-slot__half--down", children: v < 기준 && _jsx("div", { className: "manager-bar-chart-bar manager-bar-chart-bar--full manager-bar-chart-bar--down", style: { height: 높이(v), background: c } }) })] }, k))) })) : (_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));
|
|
130
161
|
})] }), _jsx("div", { className: "manager-bar-chart-x-axis", children: data.map((d, i) => {
|
|
131
162
|
const isFuture = d.a === -1; // 아직 도래하지 않은 날 — 글씨 흐리게
|
|
132
163
|
return (_jsxs("span", { className: `manager-bar-chart-x-axis__label${isFuture ? ' manager-bar-chart-x-axis__label--future' : ''}`, children: [_jsxs("span", { className: "caption-1 manager-bar-chart-x-axis__main", children: [d.isToday && _jsx("span", { className: "manager-bar-chart-x-axis__today-dot", style: { background: colorA } }), d.label] }), d.sublabel && _jsx("span", { className: "caption-1", children: d.sublabel })] }, i));
|
|
@@ -222,7 +253,7 @@ const BAR_SIZE_PRESET = {
|
|
|
222
253
|
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
223
254
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
224
255
|
*/
|
|
225
|
-
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, detailSize = 32, barGap, className, inGrid = false, }) {
|
|
256
|
+
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, detailSize = 32, barGap, baseline, className, inGrid = false, }) {
|
|
226
257
|
const { t, i18n } = useTranslation();
|
|
227
258
|
const sizePreset = BAR_SIZE_PRESET[size];
|
|
228
259
|
// 외부 control 미지정 시 내부 컨트롤 사용 (단독 차트 하위 호환)
|
|
@@ -339,8 +370,13 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
|
|
|
339
370
|
앞 달 막대). 막대 합으로 내면 8월 합계에 9/1~9/6 이 들고 8/1~8/2 가 빠져, 상세 목록(달력 달)의 건수와 어긋났다.
|
|
340
371
|
주 보기는 그 주 막대의 합 그대로. */
|
|
341
372
|
const 합칠것 = control.period === 'month' ? (data[control.monthRange]?.daily ?? []) : bars;
|
|
373
|
+
/* 기준선 모드는 부호를 살려 더한다 — 오르내림의 **순변화** 가 합계다(양수만 더하면 내린 주가 사라진다).
|
|
374
|
+
미래 표시(a === -1)는 여기서도 뺀다 — 기준선 모드에서도 -1 은 '아직 오지 않은 칸' 이다(실제 -1 이 나올 수 있으면 그 칸을 자료에서 뺀다). */
|
|
375
|
+
if (baseline != null) {
|
|
376
|
+
return 합칠것.reduce((s, d) => s + (d.a === -1 ? 0 : 수(d.a)) + (labelBKey ? 수(d.b) : 0), 0);
|
|
377
|
+
}
|
|
342
378
|
return 합칠것.reduce((s, d) => s + (d.a > 0 ? d.a : 0) + (labelBKey && d.b > 0 ? d.b : 0), 0);
|
|
343
|
-
}, [bars, rawBars, rate, rateAgg, avgMode, labelBKey, control.period, control.monthRange, data]);
|
|
379
|
+
}, [bars, rawBars, rate, rateAgg, avgMode, baseline, labelBKey, control.period, control.monthRange, data]);
|
|
344
380
|
const legendItems = series && series.length > 0
|
|
345
381
|
? series.map(s => ({ color: s.color, label: s.label }))
|
|
346
382
|
: [
|
|
@@ -357,7 +393,8 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
|
|
|
357
393
|
// 진행률/평균 모드 또는 totalLabelKey+totalFormat 조합: 라벨(윗줄) + 값(아랫줄). size='small'은 라벨 생략
|
|
358
394
|
_jsx(ChartTotal, { label: isSmall ? undefined : `${t(control.period === 'week' ? 'manager.chart.weeklyPrefix' : 'manager.chart.monthlyPrefix')} ${t(totalLabelKey ?? labelAKey)}`, children: totalFormat ? (
|
|
359
395
|
// 숫자는 h0, 단위(시간/분/초 등)는 h5로 분리
|
|
360
|
-
|
|
396
|
+
/* 부호는 숫자 쪽에 붙인다 — `[^\d]+` 로 가르면 '-7%p' 의 '-' 가 단위로 넘어가 h5 로 작아졌다(변동률) */
|
|
397
|
+
(totalFormat(total).match(/([+-]?\d[\d.,]*|[^\d]+)/g) ?? []).map((part, i) => (/\d/.test(part)
|
|
361
398
|
? _jsx("span", { className: "barchart-total__num h0", children: part }, i)
|
|
362
|
-
: _jsx("span", { className: "barchart-total__unit h5", children: part }, i)))) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "barchart-total__num h0", children: total }), totalUnitKey && _jsx("span", { className: "barchart-total__unit h5", children: t(totalUnitKey) })] })) })) : (_jsxs(ChartTotal, { children: [_jsx("span", { className: "barchart-total__num h0", children: total }), _jsx("span", { className: "barchart-total__unit h5", children: t(labelAKey) }), totalUnitKey && _jsx("span", { className: "barchart-total__unit h5", children: t(totalUnitKey) })] }))) : _jsx("span", {}), _jsx(ChartLegend, { items: legendItems, className: "caption-1" })] }), children: loading ? (_jsx("div", { className: "manager-bar-chart-loading", children: _jsx(Loader, { size: 32 }) })) : (_jsx(BarChartCanvas, { data: bars, colorA: colorA, colorB: labelBKey ? colorB : undefined, labelA: t(labelAKey), labelB: labelBKey ? t(labelBKey) : undefined, barWidth: barWidth ?? sizePreset.barWidth, barGap: barGap, 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: 나란히 })) }));
|
|
399
|
+
: _jsx("span", { className: "barchart-total__unit h5", children: part }, i)))) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "barchart-total__num h0", children: total }), totalUnitKey && _jsx("span", { className: "barchart-total__unit h5", children: t(totalUnitKey) })] })) })) : (_jsxs(ChartTotal, { children: [_jsx("span", { className: "barchart-total__num h0", children: total }), _jsx("span", { className: "barchart-total__unit h5", children: t(labelAKey) }), totalUnitKey && _jsx("span", { className: "barchart-total__unit h5", children: t(totalUnitKey) })] }))) : _jsx("span", {}), _jsx(ChartLegend, { items: legendItems, className: "caption-1" })] }), children: loading ? (_jsx("div", { className: "manager-bar-chart-loading", children: _jsx(Loader, { size: 32 }) })) : (_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: 나란히 })) }));
|
|
363
400
|
}
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
/* 좁으면 줄을 바꾼다 — 안 바꾸면 오른쪽 묶음(고르개·상세보기)이 제목 위로 올라탄다 */
|
|
25
25
|
flex-wrap: wrap;
|
|
26
26
|
gap: var(--spacing-xs);
|
|
27
|
-
|
|
27
|
+
/* 머리 바로 아래가 합계 줄이라 12px 은 떠 보인다 — 한 눈금 줄인다 */
|
|
28
|
+
margin-bottom: var(--spacing-xs);
|
|
28
29
|
}
|
|
29
30
|
/* 제목은 줄어들 수 있어야 한다 — min-width 기본값(auto)이면 글자 길이만큼 버텨 넘친다 */
|
|
30
31
|
.manager-section__title {
|
|
@@ -104,7 +105,8 @@
|
|
|
104
105
|
.barchart-total--stacked {
|
|
105
106
|
flex-direction: column;
|
|
106
107
|
align-items: flex-start;
|
|
107
|
-
|
|
108
|
+
/* 이름표와 값이 딱 붙어 한 덩어리로 읽혔다 — 눈금보다 작은 2px 만 띄운다(8px 은 두 줄이 갈라져 보인다) */
|
|
109
|
+
gap: 2px;
|
|
108
110
|
}
|
|
109
111
|
.barchart-total--stacked .barchart-total__label,
|
|
110
112
|
.barchart-total--stacked .barchart-total__num,
|
|
@@ -13,6 +13,9 @@ export type TooltipMenuColumnItem = {
|
|
|
13
13
|
disabled?: boolean;
|
|
14
14
|
/** 비활성 사유 등 hover 설명(title 속성). */
|
|
15
15
|
tooltip?: string;
|
|
16
|
+
/** 이 항목 **앞** 에 가로 구분선을 그린다 — 성격이 다른 묶음을 가를 때(보기 고르개 항목들 ↔ 창을 여는 항목).
|
|
17
|
+
* 선은 그리기만 할 뿐 항목이 아니라, 키보드 차례·낭독기 목록에 끼지 않는다(`role="separator"`). 첫 항목에 주면 무시한다. */
|
|
18
|
+
separatorBefore?: boolean;
|
|
16
19
|
/** 우측 원형 배지 숫자(알림 개수 등). 라벨에 "(5)"를 붙이는 대신 SideNav와 같은
|
|
17
20
|
* Badge type="dot"으로 표시한다 — 두 메뉴가 같은 항목을 다르게 보여주지 않도록. */
|
|
18
21
|
badgeCount?: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { Fragment, useCallback, useMemo, useState, useEffect, useRef } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import Button from '../Button';
|
|
6
6
|
import Badge from '../Badge';
|
|
@@ -81,14 +81,14 @@ export default function MenuTooltip({ columns = [], onClose, className = '', siz
|
|
|
81
81
|
}
|
|
82
82
|
catch (err) { }
|
|
83
83
|
}, [onClose, 보이는깊이]);
|
|
84
|
-
return (_jsxs("div", { className: `tooltip-menu tooltip-menu--${size} ${className}`, children: [(stack.length > 1 || current.title) && (_jsxs("div", { className: "tooltip-menu__header", children: [stack.length > 1 ? (_jsx(Button, { htmlType: "button", size: itemSize, type: "dot", icon: "chevron-left", ariaLabel: t('common.back', '뒤로'), onClick: goBack, variant: "transparent-gray", useBorder: false })) : null, _jsx("div", { className: "tooltip-menu__title button-1", children: current.title })] })), _jsx("div", { className: "tooltip-menu__columns", ref: itemsWrapRef, children: current.columns && current.columns.map((col, ci) => (_jsxs("div", { className: "tooltip-menu__column", children: [col.title ? _jsx("div", { className: "tooltip-menu__column-title body-3", children: col.title }) : null, _jsx("div", { className: "tooltip-menu__items", children: (col.items || []).map((item, idx) => (_jsx(Button, { htmlType: "button", className: "tooltip-menu__item", size: itemSize, type: item.label ? 'bar' : 'dot', icon: item.selected ? 'check' : (item.icon || 'none'), rightIcon: Array.isArray(item.children) && item.children.length
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
return (_jsxs("div", { className: `tooltip-menu tooltip-menu--${size} ${className}`, children: [(stack.length > 1 || current.title) && (_jsxs("div", { className: "tooltip-menu__header", children: [stack.length > 1 ? (_jsx(Button, { htmlType: "button", size: itemSize, type: "dot", icon: "chevron-left", ariaLabel: t('common.back', '뒤로'), onClick: goBack, variant: "transparent-gray", useBorder: false })) : null, _jsx("div", { className: "tooltip-menu__title button-1", children: current.title })] })), _jsx("div", { className: "tooltip-menu__columns", ref: itemsWrapRef, children: current.columns && current.columns.map((col, ci) => (_jsxs("div", { className: "tooltip-menu__column", children: [col.title ? _jsx("div", { className: "tooltip-menu__column-title body-3", children: col.title }) : null, _jsx("div", { className: "tooltip-menu__items", children: (col.items || []).map((item, idx) => (_jsxs(Fragment, { children: [item.separatorBefore && idx > 0 && _jsx("div", { className: "tooltip-menu__separator", role: "separator" }), _jsx(Button, { htmlType: "button", className: "tooltip-menu__item", size: itemSize, type: item.label ? 'bar' : 'dot', icon: item.selected ? 'check' : (item.icon || 'none'), rightIcon: Array.isArray(item.children) && item.children.length
|
|
85
|
+
? 'chevron-right'
|
|
86
|
+
: item.badgeCount
|
|
87
|
+
? _jsx(Badge, { type: "dot", position: "inline", number: Math.min(item.badgeCount, 99), size: 24, "aria-hidden": true })
|
|
88
|
+
: undefined, label: item.label,
|
|
89
|
+
/* 개수는 이름에 붙인다 — ariaLabel 이 글자뿐이라 배지 수가 이름에서 빠져 "알림" 으로만 읽혔다
|
|
90
|
+
(SideNav·BottomNav 와 같은 nav.badgeLabel "알림, 5개"). */
|
|
91
|
+
ariaLabel: item.label && item.badgeCount && item.badgeCount > 0
|
|
92
|
+
? t('nav.badgeLabel', { label: item.label, count: item.badgeCount, defaultValue: '{{label}}, {{count}}개' })
|
|
93
|
+
: item.label, onClick: () => handleClick(item, ci, idx), selected: item.selected, disabled: item.disabled, showTooltipInfo: item.tooltip || false, useBorder: false, align: "left" })] }, idx))) })] }, ci))) })] }));
|
|
94
94
|
}
|
|
@@ -95,6 +95,10 @@ export interface BarChartProps {
|
|
|
95
95
|
* 막대 수가 늘면 굵기는 `barWidth` 를 위 한계로 두고 줄어든다(계열 수만큼 센다).
|
|
96
96
|
*/
|
|
97
97
|
seriesMode?: 'stack' | 'group';
|
|
98
|
+
/** 0 선을 캔버스 가운데 두고 **음수는 아래로** 그린다(예: 변동률 %p). 주면 축은 위아래 대칭(±max|값|)이 되고,
|
|
99
|
+
* 눈금·격자선·이름표가 음수 구간까지 따라가며, 툴팁은 양수에 `+` 를 붙여 방향을 드러낸다.
|
|
100
|
+
* `series` + `seriesMode='group'` 과 함께 쓰면 갈래마다 0 선 위·아래로 자란다. 지금은 `0` 만 쓴다. */
|
|
101
|
+
baseline?: number;
|
|
98
102
|
/** 나란히 선 막대 사이 간격 px(기본 1). `seriesMode='group'` 에서만 쓰인다 — 한 칸에 서는 막대가 늘수록
|
|
99
103
|
* `barWidth` 와 함께 좁혀 묶음이 한 덩어리로 읽히게 한다(말귀: 5개면 굵기 6·간격 2 … 2개면 9·5). */
|
|
100
104
|
barGap?: number;
|
|
@@ -167,6 +167,9 @@ import CalendarIcon from 'lucide-react/dist/esm/icons/calendar.mjs';
|
|
|
167
167
|
import CarIcon from 'lucide-react/dist/esm/icons/car.mjs';
|
|
168
168
|
import CoffeeIcon from 'lucide-react/dist/esm/icons/coffee.mjs';
|
|
169
169
|
import Gamepad2Icon from 'lucide-react/dist/esm/icons/gamepad-2.mjs';
|
|
170
|
+
import ClockIcon from 'lucide-react/dist/esm/icons/clock.mjs';
|
|
171
|
+
import TargetIcon from 'lucide-react/dist/esm/icons/target.mjs';
|
|
172
|
+
import PercentIcon from 'lucide-react/dist/esm/icons/percent.mjs';
|
|
170
173
|
import InboxIcon from 'lucide-react/dist/esm/icons/inbox.mjs';
|
|
171
174
|
import ListTreeIcon from 'lucide-react/dist/esm/icons/list-tree.mjs';
|
|
172
175
|
import MessageSquareMoreIcon from 'lucide-react/dist/esm/icons/message-square-more.mjs';
|
|
@@ -254,6 +257,11 @@ export const STATIC_LUCIDE_ICONS = {
|
|
|
254
257
|
'folder-plus': FolderPlusIcon,
|
|
255
258
|
'gamepad': GamepadIcon,
|
|
256
259
|
'gamepad-2': Gamepad2Icon,
|
|
260
|
+
/* 시각·시간을 뜻하는 이름이 하나도 없었다(말귀 '⋯' 메뉴의 '시간' 항목) */
|
|
261
|
+
'clock': ClockIcon,
|
|
262
|
+
/* 정답률·목표 — target 과 percent 를 함께 둔다(쓰는 쪽이 뜻에 맞는 것을 고른다) */
|
|
263
|
+
'target': TargetIcon,
|
|
264
|
+
'percent': PercentIcon,
|
|
257
265
|
'gavel': GavelIcon,
|
|
258
266
|
'git-merge': GitMergeIcon,
|
|
259
267
|
'globe': GlobeIcon,
|