@ihabdevteam/core 0.162.0 → 0.164.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.
@@ -458,6 +458,9 @@ export declare interface BarData {
458
458
  isToday?: boolean;
459
459
  parts?: number[];
460
460
  partsB?: number[];
461
+ /** 아직 오지 않은 칸 — 막대를 그리지 않고 글씨를 흐리게, 평균·합계에서 뺀다.
462
+ * `a === -1` 이 같은 뜻의 옛 표시인데, **음수가 값인 차트**(baseline)에서는 −1 이 진짜 값일 수 있어 이 칸을 쓴다. */
463
+ future?: boolean;
461
464
  }
462
465
 
463
466
  /**
@@ -1013,6 +1016,8 @@ export declare interface ChartPoint {
1013
1016
  b: number;
1014
1017
  parts?: number[];
1015
1018
  partsB?: number[];
1019
+ /** 아직 오지 않은 날 — `a === -1` 과 같은 뜻이고, 음수를 값으로 쓰는 차트(baseline)에서는 이쪽을 쓴다. */
1020
+ future?: boolean;
1016
1021
  }
1017
1022
 
1018
1023
  /**
@@ -1500,7 +1505,7 @@ export declare function copyText(text: string): Promise<boolean>;
1500
1505
  *
1501
1506
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
1502
1507
  */
1503
- export declare const CORE_VERSION = "0.162.0";
1508
+ export declare const CORE_VERSION = "0.164.0";
1504
1509
 
1505
1510
  /** @public */
1506
1511
  declare const coreDefault: Record<string, unknown>;
@@ -7023,6 +7028,9 @@ export declare type TooltipMenuColumnItem = {
7023
7028
  disabled?: boolean;
7024
7029
  /** 비활성 사유 등 hover 설명(title 속성). */
7025
7030
  tooltip?: string;
7031
+ /** 이 항목 **앞** 에 가로 구분선을 그린다 — 성격이 다른 묶음을 가를 때(보기 고르개 항목들 ↔ 창을 여는 항목).
7032
+ * 선은 그리기만 할 뿐 항목이 아니라, 키보드 차례·낭독기 목록에 끼지 않는다(`role="separator"`). 첫 항목에 주면 무시한다. */
7033
+ separatorBefore?: boolean;
7026
7034
  /** 우측 원형 배지 숫자(알림 개수 등). 라벨에 "(5)"를 붙이는 대신 SideNav와 같은
7027
7035
  * Badge type="dot"으로 표시한다 — 두 메뉴가 같은 항목을 다르게 보여주지 않도록. */
7028
7036
  badgeCount?: number;
@@ -16,24 +16,31 @@ 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
+ /** 아직 오지 않은 칸인가 — 옛 표시(a === -1)와 새 표시(future)를 함께 본다.
20
+ * 음수가 값인 차트(baseline)에서는 −1 이 진짜 값일 수 있어 `future` 를 쓴다. */
21
+ function 미래칸(d, 옛표시 = true) {
22
+ /* `baseline` 을 쓰는 차트는 −1 이 진짜 값이라 옛 표시를 보지 않는다 — 부르는 쪽이 옛표시=false 로 끈다 */
23
+ return d.future === true || (옛표시 && d.a === -1);
24
+ }
19
25
  function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, barGap = 1, height = 140, maxOverride, tightMax, seriesColors, seriesLabels, rateLabels, grouped = false, baseline }) {
20
26
  // colorB 미지정 시 단일 계열(A) 모드
21
27
  const single = !colorB;
22
28
  const stacked = !!(seriesColors && seriesColors.length > 0);
23
29
  /* 값이 비었거나(undefined) 수가 아니면 0 으로 친다. 두 계열인데 한 항목의 `b` 가 비면 Math.max 가 NaN 이 되고
24
30
  `NaN || 10` 으로 y축이 데이터와 상관없이 10 에 박혀 막대가 판 위로 넘쳤다(ihab-core-96 이 짚음). */
31
+ /* 기준선 모드에서는 옛 미래 표시(a === -1)를 보지 않는다 — −1 이 진짜 값이다 */
32
+ const 기준 = baseline;
25
33
  const maxVal = useMemo(() => stacked
26
- ? Math.max(0, ...data.filter(d => d.a !== -1).map(d => grouped
34
+ ? Math.max(0, ...data.filter(d => !미래칸(d, 기준 == null)).map(d => grouped
27
35
  /* 나란히 서면 가장 높은 **낱 막대**가 위를 정한다 — 합으로 잡으면 막대가 판의 몇 분의 일로 눌린다 */
28
36
  ? Math.max(0, ...(d.parts ?? []).map(수))
29
37
  : (d.parts ?? []).reduce((s, v) => s + Math.max(0, 수(v)), 0)))
30
- : Math.max(0, ...data.filter(d => d.a !== -1).flatMap(d => single ? [수(d.a)] : [수(d.a), 수(d.b)])), [data, single, stacked, grouped]);
38
+ : Math.max(0, ...data.filter(d => !미래칸(d, 기준 == null)).flatMap(d => single ? [수(d.a)] : [수(d.a), 수(d.b)])), [data, single, stacked, grouped]);
31
39
  /* 기준선 모드: 0 선에서 얼마나 떨어졌는지(|값 − 기준|)가 축을 정한다 — 위아래가 대칭이라야 같은 크기의 오름·내림이 같은 길이로 읽힌다 */
32
- const 기준 = baseline;
33
40
  const 대칭최대 = useMemo(() => {
34
41
  if (기준 == null)
35
42
  return 0;
36
- const 값들 = data.filter(d => d.a !== -1).flatMap(d => stacked ? (d.parts ?? []).map(수) : (single ? [수(d.a)] : [수(d.a), 수(d.b)]));
43
+ const 값들 = data.filter(d => !미래칸(d, 기준 == null)).flatMap(d => stacked ? (d.parts ?? []).map(수) : (single ? [수(d.a)] : [수(d.a), 수(d.b)]));
37
44
  return Math.max(0, ...값들.map(v => Math.abs(v - 기준)));
38
45
  }, [data, 기준, stacked, single]);
39
46
  const padded = useMemo(() => {
@@ -91,7 +98,7 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, b
91
98
  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) => (
92
99
  /* 눈금선은 **적힌 값의 높이**에 둔다 — 가운데 값을 반올림(최대 3 → '2')하면서 자리는 절반(1.5)에 두어 선과 숫자가 어긋났다 */
93
100
  _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) => {
94
- const isFuture = d.a === -1;
101
+ const isFuture = 미래칸(d, 기준 == null);
95
102
  /* 판 높이를 넘지 않는다 — maxOverride(진행률 100)보다 큰 값이 와도 막대가 눈금 위로 삐져나가지 않게 */
96
103
  /* 기준선 모드는 반쪽(h/2)을 쓴다 — 0 선에서 위나 아래로 자란다 */
97
104
  const 반 = h / 2;
@@ -159,7 +166,7 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, b
159
166
  });
160
167
  })() })) : (기준 != 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));
161
168
  })] }), _jsx("div", { className: "manager-bar-chart-x-axis", children: data.map((d, i) => {
162
- const isFuture = d.a === -1; // 아직 도래하지 않은 날 — 글씨 흐리게
169
+ const isFuture = 미래칸(d, 기준 == null); // 아직 도래하지 않은 날 — 글씨 흐리게
163
170
  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));
164
171
  }) })] }) }) }), tooltip && (_jsx(DescriptionTooltip, { anchorRect: tooltip.rect, title: tooltip.title, items: tooltip.items, showIndex: false, closeOnOutsideClick: false }))] }));
165
172
  }
@@ -180,18 +187,18 @@ export function BarChartControls({ control }) {
180
187
  * 주 보기의 막대·합계와 월 보기 완료율 주 막대가 **이 한 벌**을 쓴다 — 같은 주가 두 보기에서
181
188
  * 다른 수로 서지 않게(2026-09-12 말귀 검수: 주 보기 25% · 월 보기 38%).
182
189
  */
183
- function 주의날들(data, monday, now) {
190
+ function 주의날들(data, monday, now, 옛표시 = true) {
184
191
  const 오늘 = new Date(now.getFullYear(), now.getMonth(), now.getDate());
185
192
  return Array.from({ length: 7 }, (_, i) => {
186
193
  const dt = new Date(monday);
187
194
  dt.setDate(monday.getDate() + i);
188
195
  const entry = (data[`${dt.getFullYear()}년 ${dt.getMonth() + 1}월`]?.daily ?? [])[dt.getDate() - 1];
189
- const isFuture = entry?.a === -1 || dt > 오늘; // 미래 날 — 평균/막대에서 제외
196
+ const isFuture = (entry != null && 미래칸(entry, 옛표시)) || dt > 오늘; // 미래 날 — 평균/막대에서 제외
190
197
  return { dt, entry, isFuture };
191
198
  });
192
199
  }
193
200
  /** control(기간)에 따라 data를 막대 배열로 변환 */
194
- function barsFromControl(data, control, t, lang) {
201
+ function barsFromControl(data, control, t, lang, 옛표시 = true) {
195
202
  const { period, monthRange, weekRange, weekRanges, DOW } = control;
196
203
  const isKo = lang?.startsWith('ko');
197
204
  const now = getTodayKstDate(); // 칸은 KST 달력 — 오늘·이번 주 표시도 KST 로
@@ -220,19 +227,20 @@ function barsFromControl(data, control, t, lang) {
220
227
  // 시작일이 오늘 이후(또는 미래 달)면 아직 도래하지 않은 주 → 흐리게/막대 미표시
221
228
  isFutureWeek = year > nowY || (year === nowY && (month > nowM || (month === nowM && startDay > nowD)));
222
229
  }
223
- return { ...d, label, sublabel: isKo ? `${KO_ORD[n - 1] ?? n}주` : `Week ${n}`, isToday, a: isFutureWeek ? -1 : d.a, b: isFutureWeek ? 0 : d.b };
230
+ return { ...d, label, sublabel: isKo ? `${KO_ORD[n - 1] ?? n}주` : `Week ${n}`, isToday, future: (d.future === true || isFutureWeek) || undefined, a: isFutureWeek ? -1 : d.a, b: isFutureWeek ? 0 : d.b };
224
231
  });
225
232
  }
226
233
  const opt = weekRanges.find(o => o.value === weekRange);
227
234
  if (!opt)
228
235
  return [];
229
236
  const monday = new Date(opt.year, opt.month - 1, firstMondayOfMonth(opt.year, opt.month - 1) + opt.week * 7);
230
- return 주의날들(data, monday, now).map(({ dt, entry, isFuture }) => {
237
+ return 주의날들(data, monday, now, 옛표시).map(({ dt, entry, isFuture }) => {
231
238
  const dM = dt.getMonth() + 1, dD = dt.getDate(), dY = dt.getFullYear();
232
239
  const sameMonth = dM === opt.month && dY === opt.year;
233
240
  return {
234
241
  label: sameMonth ? `${dD}` : `${dM}/${dD}`,
235
242
  sublabel: DOW[dt.getDay()] ?? '',
243
+ future: isFuture || undefined,
236
244
  a: isFuture ? -1 : (entry && entry.a > 0 ? entry.a : 0),
237
245
  b: isFuture ? 0 : (entry && entry.b > 0 ? entry.b : 0),
238
246
  isToday: dY === nowY && dM === nowM && dD === nowD,
@@ -259,7 +267,7 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
259
267
  // 외부 control 미지정 시 내부 컨트롤 사용 (단독 차트 하위 호환)
260
268
  const internalControl = useBarChartControl(data);
261
269
  const control = controlProp ?? internalControl;
262
- const rawBars = useMemo(() => barsFromControl(data, control, t, i18n.language), [data, control, t, i18n.language]);
270
+ const rawBars = useMemo(() => barsFromControl(data, control, t, i18n.language, baseline == null), [data, control, t, i18n.language, baseline]);
263
271
  // 완료율 모드: a = 진행률(a/b×100). labelBKey 지정 시 b = 정답률(parts[0]/parts[1]×100) 2계열.
264
272
  // 월 보기의 주 막대는 **그 주 경과한 날의 일별 진행률 평균(미기록일 0%)** — 아래 total 과
265
273
  // 주 보기가 쓰는 같은 셈. 주 합 a/b 로 두면 기록 없는 날(b=0)이 빠져 같은 주가 두 보기에서
@@ -299,7 +307,7 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
299
307
  };
300
308
  return rawBars.map((d, i) => {
301
309
  let a = d.a === -1 ? -1 : (d.b > 0 ? Math.round((d.a / d.b) * 100) : 0);
302
- if (월 && d.a !== -1) {
310
+ if (월 && !미래칸(d)) {
303
311
  const y = Number(월[1]), m0 = Number(월[2]) - 1;
304
312
  const 지난날 = 주의날들(data, new Date(y, m0, firstMondayOfMonth(y, m0) + i * 7), now).filter(x => !x.isFuture);
305
313
  if (rateAgg === 'weighted') {
@@ -319,7 +327,7 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
319
327
  }
320
328
  }
321
329
  let 계열 = null;
322
- if (나란히 && d.a !== -1) {
330
+ if (나란히 && !미래칸(d)) {
323
331
  let 날들;
324
332
  if (월) {
325
333
  const y = Number(월[1]), m0 = Number(월[2]) - 1;
@@ -373,7 +381,7 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
373
381
  /* 기준선 모드는 부호를 살려 더한다 — 오르내림의 **순변화** 가 합계다(양수만 더하면 내린 주가 사라진다).
374
382
  미래 표시(a === -1)는 여기서도 뺀다 — 기준선 모드에서도 -1 은 '아직 오지 않은 칸' 이다(실제 -1 이 나올 수 있으면 그 칸을 자료에서 뺀다). */
375
383
  if (baseline != null) {
376
- return 합칠것.reduce((s, d) => s + (d.a === -1 ? 0 : 수(d.a)) + (labelBKey ? 수(d.b) : 0), 0);
384
+ return 합칠것.reduce((s, d) => s + (미래칸(d, false) ? 0 : 수(d.a) + (labelBKey ? 수(d.b) : 0)), 0);
377
385
  }
378
386
  return 합칠것.reduce((s, d) => s + (d.a > 0 ? d.a : 0) + (labelBKey && d.b > 0 ? d.b : 0), 0);
379
387
  }, [bars, rawBars, rate, rateAgg, avgMode, baseline, labelBKey, control.period, control.monthRange, data]);
@@ -389,7 +397,7 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
389
397
  const isLarge = size === 'large';
390
398
  const sectionClassName = [isSmall ? 'manager-section--compact' : isLarge ? 'manager-section--full' : undefined, className]
391
399
  .filter(Boolean).join(' ') || undefined;
392
- return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: isSmall ? (headExtra ?? undefined) : _jsxs(_Fragment, { children: [!hideControls && _jsx(BarChartControls, { control: control }), headExtra, onDetail && (_jsx("span", { className: "manager-bar-chart-detail-btn", children: _jsx(Button, { htmlType: "button", size: detailSize, type: "dot", icon: "list", ariaLabel: t('trainingStatus.detail', '상세보기'), onClick: onDetail }) }))] }), subRow: _jsxs(_Fragment, { children: [showTotal ? ((rate || avgMode || (!!totalLabelKey && !!totalFormat)) ? (
400
+ return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: isSmall ? (headExtra ?? undefined) : _jsxs(_Fragment, { children: [!hideControls && _jsx(BarChartControls, { control: control }), headExtra, onDetail && (_jsx("span", { className: "manager-bar-chart-detail-btn", children: _jsx(Button, { htmlType: "button", size: detailSize, type: "dot", icon: "list", ariaLabel: t('trainingStatus.detail', '상세보기'), showTooltipInfo: t('trainingStatus.detail', '상세보기'), onClick: onDetail }) }))] }), subRow: _jsxs(_Fragment, { children: [showTotal ? ((rate || avgMode || (!!totalLabelKey && !!totalFormat)) ? (
393
401
  // 진행률/평균 모드 또는 totalLabelKey+totalFormat 조합: 라벨(윗줄) + 값(아랫줄). size='small'은 라벨 생략
394
402
  _jsx(ChartTotal, { label: isSmall ? undefined : `${t(control.period === 'week' ? 'manager.chart.weeklyPrefix' : 'manager.chart.monthlyPrefix')} ${t(totalLabelKey ?? labelAKey)}`, children: totalFormat ? (
395
403
  // 숫자는 h0, 단위(시간/분/초 등)는 h5로 분리
@@ -63,6 +63,13 @@
63
63
  padding: 6px 10px;
64
64
  }
65
65
 
66
+ /* 묶음 사이 가로선 — 항목이 아니라 경계다(키보드 차례에 끼지 않는다) */
67
+ .tooltip-menu__separator {
68
+ height: 1px;
69
+ margin: 4px 0;
70
+ background: var(--color-whiteLine);
71
+ }
72
+
66
73
  .tooltip-menu__item {
67
74
  width: 100%;
68
75
  }
@@ -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
- ? '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))) })] }));
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
  }
@@ -11,6 +11,9 @@ export interface BarData {
11
11
  isToday?: boolean;
12
12
  parts?: number[];
13
13
  partsB?: number[];
14
+ /** 아직 오지 않은 칸 — 막대를 그리지 않고 글씨를 흐리게, 평균·합계에서 뺀다.
15
+ * `a === -1` 이 같은 뜻의 옛 표시인데, **음수가 값인 차트**(baseline)에서는 −1 이 진짜 값일 수 있어 이 칸을 쓴다. */
16
+ future?: boolean;
14
17
  }
15
18
  /** 한 구간(월 등)의 주별/일별 막대 데이터 묶음 */
16
19
  export type MonthBarData = {
@@ -26,6 +29,8 @@ export interface ChartPoint {
26
29
  b: number;
27
30
  parts?: number[];
28
31
  partsB?: number[];
32
+ /** 아직 오지 않은 날 — `a === -1` 과 같은 뜻이고, 음수를 값으로 쓰는 차트(baseline)에서는 이쪽을 쓴다. */
33
+ future?: boolean;
29
34
  }
30
35
  /** 해당 월(0-based)의 첫 월요일 날짜(1~7). 예: 2026-05(금요일 시작) → 4 */
31
36
  export declare function firstMondayOfMonth(year: number, month0: number): number;
@@ -113,6 +113,8 @@ export function buildMonthChartData(points) {
113
113
  const isFuture = isCurMonth && d > today;
114
114
  daily.push({
115
115
  label: String(d),
116
+ /* 미래는 두 표시를 함께 단다 — 음수를 값으로 쓰는 차트(baseline)는 -1 을 진짜 값으로 읽으므로 future 를 본다 */
117
+ ...(isFuture && dayA[d] === 0 && dayB[d] === 0 ? { future: true } : {}),
116
118
  a: isFuture && dayA[d] === 0 && dayB[d] === 0 ? -1 : dayA[d],
117
119
  b: dayB[d],
118
120
  ...(partsLen ? { parts: dayParts[d] } : {}),
@@ -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,
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.162.0";
49
+ export declare const CORE_VERSION = "0.164.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.162.0';
49
+ export const CORE_VERSION = '0.164.0';