@ihabdevteam/core 0.246.0 → 0.248.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 +72 -0
- package/dist/components.cjs.js +179 -179
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +183 -183
- package/dist/ihabdevteam-core.d.ts +32 -5
- package/dist/packages/core/src/components/BarChart.d.ts +1 -1
- package/dist/packages/core/src/components/BarChart.js +2 -2
- package/dist/packages/core/src/components/Calendar.js +10 -3
- package/dist/packages/core/src/components/Panel.d.ts +9 -1
- package/dist/packages/core/src/components/Panel.js +3 -2
- package/dist/packages/core/src/components/PanelRow.d.ts +6 -1
- package/dist/packages/core/src/components/PanelRow.js +3 -1
- package/dist/packages/core/src/components/RingChart.d.ts +4 -2
- package/dist/packages/core/src/components/RingChart.js +2 -2
- package/dist/packages/core/src/components/TextField.d.ts +6 -0
- package/dist/packages/core/src/components/TextField.js +4 -2
- package/dist/packages/core/src/components/Training/CategoryIcon.js +3 -1
- package/dist/packages/core/src/components/barChartTypes.d.ts +7 -1
- package/dist/packages/core/src/i18n/locales/en.json +1 -0
- package/dist/packages/core/src/i18n/locales/ko.json +1 -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({ 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;
|
|
313
|
+
export declare function BarChart({ focusableBars, size, barWidth, titleKey, titleLevel, 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 {
|
|
@@ -376,6 +376,12 @@ export declare interface BarChartProps {
|
|
|
376
376
|
barWidth?: number;
|
|
377
377
|
/** 섹션 제목 i18n 키 */
|
|
378
378
|
titleKey: string;
|
|
379
|
+
/** 제목을 머리글 몇 급으로 그릴지 — 그대로 `ChartSection` 에 넘긴다(말귀 요청 (60)③, 2026-10-02).
|
|
380
|
+
*
|
|
381
|
+
* `ChartSection` 은 (51) 에서 급을 열었는데 **막대·링은 그 문을 안 달았다.** 그래서 칸을
|
|
382
|
+
* 부르는 쪽이 급을 못 정하고 늘 기본 3 이 나갔다 — 대시보드 제목이 `h1` 이면 층이
|
|
383
|
+
* h1 → h3 로 한 급 비어 읽힌다. 자세한 뜻은 `ChartSectionProps.titleLevel` 참고. */
|
|
384
|
+
titleLevel?: ChartSectionProps['titleLevel'];
|
|
379
385
|
/** 구간(월) 키 → 주별/일별 데이터 */
|
|
380
386
|
data: Record<string, MonthBarData>;
|
|
381
387
|
colorA: string;
|
|
@@ -1645,7 +1651,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1645
1651
|
*
|
|
1646
1652
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1647
1653
|
*/
|
|
1648
|
-
export declare const CORE_VERSION = "0.
|
|
1654
|
+
export declare const CORE_VERSION = "0.248.0";
|
|
1649
1655
|
|
|
1650
1656
|
/** @public */
|
|
1651
1657
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -4189,7 +4195,7 @@ export declare interface PairStats {
|
|
|
4189
4195
|
total: number;
|
|
4190
4196
|
}
|
|
4191
4197
|
|
|
4192
|
-
export declare function Panel({ title, icon, disc, actions, size, minHeight, maxWidth, className, scrollable, scrollbarGutter, compactRows, useBorder, useBg, useRowBorder, labelWidth, form, loading, empty, children }: PanelProps): JSX.Element;
|
|
4198
|
+
export declare function Panel({ title, titleLevel, icon, disc, actions, size, minHeight, maxWidth, className, scrollable, scrollbarGutter, compactRows, useBorder, useBg, useRowBorder, labelWidth, form, loading, empty, children }: PanelProps): JSX.Element;
|
|
4193
4199
|
|
|
4194
4200
|
/** @public */
|
|
4195
4201
|
export declare type PanelElement = HTMLDivElement & {
|
|
@@ -4204,6 +4210,14 @@ export declare function PanelGrid({ cols, children }: {
|
|
|
4204
4210
|
}): JSX.Element;
|
|
4205
4211
|
|
|
4206
4212
|
declare interface PanelProps {
|
|
4213
|
+
/** 제목을 **머리글 몇 급**으로 그릴지(말귀 요청 (59), 2026-10-02).
|
|
4214
|
+
*
|
|
4215
|
+
* 기본 2 는 전과 같다 — **창 제목이 머리글이 아니던 때**의 값이다. 0.242.0 에서 창
|
|
4216
|
+
* 제목이 `<h2>` 가 되면서, 제목 있는 `Panel` 을 품은 창마다 둘이 **같은 급**으로 서게
|
|
4217
|
+
* 됐다(말귀 실측: 훈련 묶음 추가 창에 창 제목 말고 `h2` 가 둘). 머리글로 훑으면 판이
|
|
4218
|
+
* 창의 아래가 아니라 **옆**으로 읽힌다 — 창 안에서는 3 으로 내린다.
|
|
4219
|
+
* `0` 이면 머리글을 안 세운다(글자 없는 제목은 0.235.0 부터 애초에 안 선다). */
|
|
4220
|
+
titleLevel?: 0 | 2 | 3 | 4 | 5 | 6;
|
|
4207
4221
|
title?: ReactNode;
|
|
4208
4222
|
/**
|
|
4209
4223
|
* 제목 앞에 서는 lucide 아이콘 이름. Section 의 `icon` 과 같은 자리다.
|
|
@@ -4351,7 +4365,12 @@ declare type PanelRowInput = Base & {
|
|
|
4351
4365
|
disabled?: boolean;
|
|
4352
4366
|
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
|
|
4353
4367
|
autoComplete?: string;
|
|
4354
|
-
|
|
4368
|
+
/** 칸 아래 안내 한 줄 — `aria-describedby` 로 **칸과 이어진다**(말귀 요청 (60)①, 2026-10-02).
|
|
4369
|
+
*
|
|
4370
|
+
* `ReactNode` 다 — 앱이 `<span role="status">` 로 감싸 **값에 따라 바뀌는 안내**를
|
|
4371
|
+
* 그 자리에서 알릴 수 있게 한 것이다. `error` 로 보내면 될 일이 아니다: `error` 는
|
|
4372
|
+
* `aria-invalid` 까지 세워 "틀린 값" 이라고 말하는데, 안내는 틀렸다는 말이 아니다. */
|
|
4373
|
+
helperText?: ReactNode;
|
|
4355
4374
|
/** 최대 글자 수 — 설정 시 입력 제한 + 박스 안쪽 글자수 카운터 표시 */
|
|
4356
4375
|
maxLength?: number;
|
|
4357
4376
|
/** 글자수 카운터 표시 여부 (maxLength 설정 시, 기본 true) */
|
|
@@ -6059,7 +6078,7 @@ export declare function reviewTableName(original: string | ReviewSource, today:
|
|
|
6059
6078
|
* 미지정 시에는 헤더 없는 순수 링만 렌더된다(크기/색상 데모 등).
|
|
6060
6079
|
* @public
|
|
6061
6080
|
*/
|
|
6062
|
-
export declare function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid, onClick, ariaLabel, tooltip, ...ringProps }: RingChartProps): JSX.Element;
|
|
6081
|
+
export declare function RingChart({ titleKey, titleLevel, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid, onClick, ariaLabel, tooltip, ...ringProps }: RingChartProps): JSX.Element;
|
|
6063
6082
|
|
|
6064
6083
|
/** @public */
|
|
6065
6084
|
export declare interface RingChartProps {
|
|
@@ -6096,6 +6115,8 @@ export declare interface RingChartProps {
|
|
|
6096
6115
|
* 감싼 카드로 렌더. 미지정 시 헤더 없는 순수 링만 렌더(기존 동작과 동일).
|
|
6097
6116
|
*/
|
|
6098
6117
|
titleKey?: string;
|
|
6118
|
+
/** 제목 머리글 급 — 그대로 `ChartSection` 에 넘긴다(`titleKey` 지정 시에만). `BarChartProps.titleLevel` 과 같은 뜻 */
|
|
6119
|
+
titleLevel?: ChartSectionProps['titleLevel'];
|
|
6099
6120
|
/** 헤더 우측 영역 — 기간 네비게이션, 상세보기 버튼 등 (titleKey 지정 시에만 사용) */
|
|
6100
6121
|
headRight?: ReactNode;
|
|
6101
6122
|
/** 제목 옆 아이콘(옅게 표시, StatChart와 동일한 톤) — 지정 시 headRight 자리에 렌더(titleKey 지정 시에만 사용) */
|
|
@@ -7580,6 +7601,12 @@ export declare interface TextFieldProps {
|
|
|
7580
7601
|
label?: string;
|
|
7581
7602
|
/** 화면에 보이는 라벨 없이(placeholder만 쓸 때) 스크린리더용 이름을 지정. label이 있으면 무시됨 */
|
|
7582
7603
|
ariaLabel?: string;
|
|
7604
|
+
/** **바깥에서 그린 설명**의 id — 이 칸 안의 `helperText`·`error` id 와 **함께** 걸린다(덮지 않는다).
|
|
7605
|
+
*
|
|
7606
|
+
* `PanelRow` 처럼 감싸는 쪽이 도움말을 제 자리에 그리는 자리가 있다. 전에는 그 글이 칸과
|
|
7607
|
+
* 이어지지 않아, 보이기만 하고 낭독기에는 영 안 갔다(말귀 요청 (60)①, 2026-10-02).
|
|
7608
|
+
* 빈칸 띄움으로 여러 개도 된다 — `aria-describedby` 는 id 목록이다. */
|
|
7609
|
+
ariaDescribedBy?: string;
|
|
7583
7610
|
/** 필수 항목 표시 (*) */
|
|
7584
7611
|
required?: boolean;
|
|
7585
7612
|
/** placeholder 텍스트 */
|
|
@@ -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({ 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;
|
|
16
|
+
export default function BarChart({ focusableBars, size, barWidth, titleKey, titleLevel, 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;
|
|
@@ -290,7 +290,7 @@ const BAR_SIZE_PRESET = {
|
|
|
290
290
|
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
291
291
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
292
292
|
*/
|
|
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, }) {
|
|
293
|
+
export default function BarChart({ focusableBars, size = 'medium', barWidth, titleKey, titleLevel, 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, }) {
|
|
294
294
|
const { t, i18n } = useTranslation();
|
|
295
295
|
const sizePreset = BAR_SIZE_PRESET[size];
|
|
296
296
|
// 외부 control 미지정 시 내부 컨트롤 사용 (단독 차트 하위 호환)
|
|
@@ -428,7 +428,7 @@ export default function BarChart({ focusableBars, size = 'medium', barWidth, tit
|
|
|
428
428
|
.filter(Boolean).join(' ') || undefined;
|
|
429
429
|
return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, icon: icon, disc: disc, onClick: onClick, ariaLabel: cardAriaLabel, tooltip: cardTooltip, headSegment: headSegment,
|
|
430
430
|
/* 좁아 고르개가 '⋯' 로 접히면 상세보기도 그 메뉴로 들어간다 — 머리에 단추 둘이 남지 않게 */
|
|
431
|
-
headMenuItems: onDetail ? [{ label: t('trainingStatus.detail', '상세보기'), icon: 'list', onClick: onDetail, closeOnSelect: true }] : undefined, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: isSmall ? (headExtra ?? undefined) : ({ collapsed }) => _jsxs(_Fragment, { children: [!hideControls && _jsx(BarChartControls, { control: control }), headExtra, onDetail && !collapsed && (_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)) ? (
|
|
431
|
+
headMenuItems: onDetail ? [{ label: t('trainingStatus.detail', '상세보기'), icon: 'list', onClick: onDetail, closeOnSelect: true }] : undefined, title: t(titleKey), titleLevel: titleLevel, titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: isSmall ? (headExtra ?? undefined) : ({ collapsed }) => _jsxs(_Fragment, { children: [!hideControls && _jsx(BarChartControls, { control: control }), headExtra, onDetail && !collapsed && (_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)) ? (
|
|
432
432
|
// 진행률/평균 모드 또는 totalLabelKey+totalFormat 조합: 라벨(윗줄) + 값(아랫줄). size='small'은 라벨 생략
|
|
433
433
|
_jsx(ChartTotal, { label: isSmall ? undefined : `${t(control.period === 'week' ? 'manager.chart.weeklyPrefix' : 'manager.chart.monthlyPrefix')} ${t(totalLabelKey ?? labelAKey)}`, children: totalFormat ? (
|
|
434
434
|
// 숫자는 h0, 단위(시간/분/초 등)는 h5로 분리
|
|
@@ -4,14 +4,14 @@ import { useCallback, useMemo, useState, useRef } from 'react';
|
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import Icon from './Icon';
|
|
6
6
|
import { formatTimeHM } from '../utils/format';
|
|
7
|
-
import { todayYmdOr } from '../utils/date';
|
|
7
|
+
import { todayYmdOr, formatDateLocale } from '../utils/date';
|
|
8
8
|
import './Calendar.css';
|
|
9
9
|
import { pressable } from '../utils/pressable';
|
|
10
10
|
function daysInMonth(year, month) { return new Date(year, month + 1, 0).getDate(); }
|
|
11
11
|
function firstDayOfMonth(year, month) { return new Date(year, month, 1).getDay(); }
|
|
12
12
|
/** @public */
|
|
13
13
|
export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDayClick, today, }) {
|
|
14
|
-
const { t } = useTranslation();
|
|
14
|
+
const { t, i18n } = useTranslation();
|
|
15
15
|
const DOW = useMemo(() => t('calendar.dow', { returnObjects: true }), [t]);
|
|
16
16
|
/* '오늘' 은 KST 의 날 — 방문 날짜가 KST 달력(toKstYmd)이라, 기기 달력으로 열면 해외 기기에서 오늘 방문이 안 보이는 달에 있고
|
|
17
17
|
오늘 표시가 다른 날에 붙었다(해외 기기 공식 지원) */
|
|
@@ -61,6 +61,8 @@ export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDay
|
|
|
61
61
|
const day = d;
|
|
62
62
|
const dateKey = fmtDate(day);
|
|
63
63
|
const dayVisits = visitsByDate[dateKey] ?? [];
|
|
64
|
+
/* 낭독기에 나가는 '며칠' — 보는 locale 을 따라 글자꼴만 바뀌고 가리키는 날은 KST 하나다(utils/date 머리글) */
|
|
65
|
+
const 날짜말 = formatDateLocale(dateKey, i18n.language);
|
|
64
66
|
const isDragOver = dragOverDate === dateKey;
|
|
65
67
|
return (_jsxs("div", { className: `calendar-cell${isToday(day) ? ' today' : ''}${isDragOver ? ' drag-over' : ''}${onDayClick ? ' calendar-cell--clickable' : ''}`, role: onDayClick ? 'button' : undefined, tabIndex: onDayClick ? 0 : undefined, "aria-label": onDayClick ? t('calendar.addOnDate', { date: dateKey }) : undefined, "aria-current": isToday(day) ? 'date' : undefined, onClick: onDayClick ? () => onDayClick(dateKey) : undefined,
|
|
66
68
|
/* `<div role="button">` 은 브라우저가 Enter·Space 를 눌림으로 바꿔 주지 않는다 */
|
|
@@ -83,13 +85,18 @@ export default function Calendar({ visits = [], onVisitClick, onVisitMove, onDay
|
|
|
83
85
|
dragVisitId.current = null;
|
|
84
86
|
}
|
|
85
87
|
setDragOverDate(null);
|
|
86
|
-
} : undefined, children: [_jsx("div", { className: "calendar-cell__number body-3", children: day }), dayVisits.length > 0 && (_jsxs("span", { className: "calendar-cell__visit-count caption-1", "aria-label": t('calendar.visitCount', { count: dayVisits.length, defaultValue: '방문 {{count}}건' })
|
|
88
|
+
} : undefined, children: [_jsx("div", { className: "calendar-cell__number body-3", children: day }), dayVisits.length > 0 && (_jsxs("span", { className: "calendar-cell__visit-count caption-1", "aria-label": `${날짜말} ${t('calendar.visitCount', { count: dayVisits.length, defaultValue: '방문 {{count}}건' })}`, children: [_jsx("span", { className: "calendar-cell__visit-dot", "aria-hidden": "true" }), dayVisits.length] })), _jsx("div", { className: "calendar-cell__badges", children: dayVisits.map((v, vi) => (_jsxs("div", { draggable: !!onVisitMove, className: `calendar-visit-badge${v.status === 'noVisit' ? ' calendar-visit-badge--no-visit' : v.status === 'scheduled' ? ' calendar-visit-badge--scheduled' : ''}${onVisitClick ? ' calendar-visit-badge--clickable' : ''}${onVisitMove ? ' calendar-visit-badge--draggable' : ''}`, onDragStart: onVisitMove ? (e) => {
|
|
87
89
|
dragVisitId.current = v.id;
|
|
88
90
|
e.dataTransfer.effectAllowed = 'move';
|
|
89
91
|
} : undefined, onDragEnd: onVisitMove ? () => {
|
|
90
92
|
dragVisitId.current = null;
|
|
91
93
|
setDragOverDate(null);
|
|
92
94
|
} : undefined, onClick: onVisitClick ? (e) => { e.stopPropagation(); onVisitClick(v); } : undefined, ...pressable(onVisitClick ? () => onVisitClick(v) : undefined), "aria-label": [
|
|
95
|
+
// **며칠**이 빠져 있었다 — 칸 번호(1~31)는 `aria-hidden` 도 아니지만 이름표의
|
|
96
|
+
// 이름을 대체하므로 낭독기에는 "12시 55분 지한 예약" 만 가고, 어느 날 것인지는
|
|
97
|
+
// 안 간다. 격자 꼴(`gridcell`)로 날을 함께 읽히는 길도 있지만 그건 화살표
|
|
98
|
+
// 이동까지 약속하는 것이라, 지금은 이름 안에 날을 적는다(말귀 요청 (60)②).
|
|
99
|
+
날짜말,
|
|
93
100
|
v.time ? formatTimeHM(v.time, t) : null,
|
|
94
101
|
v.customer,
|
|
95
102
|
t(v.status === 'noVisit' ? 'calendar.legend.noVisit'
|
|
@@ -2,6 +2,14 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
import './Panel.css';
|
|
3
3
|
type PanelSize = 'small' | 'medium' | 'large';
|
|
4
4
|
interface PanelProps {
|
|
5
|
+
/** 제목을 **머리글 몇 급**으로 그릴지(말귀 요청 (59), 2026-10-02).
|
|
6
|
+
*
|
|
7
|
+
* 기본 2 는 전과 같다 — **창 제목이 머리글이 아니던 때**의 값이다. 0.242.0 에서 창
|
|
8
|
+
* 제목이 `<h2>` 가 되면서, 제목 있는 `Panel` 을 품은 창마다 둘이 **같은 급**으로 서게
|
|
9
|
+
* 됐다(말귀 실측: 훈련 묶음 추가 창에 창 제목 말고 `h2` 가 둘). 머리글로 훑으면 판이
|
|
10
|
+
* 창의 아래가 아니라 **옆**으로 읽힌다 — 창 안에서는 3 으로 내린다.
|
|
11
|
+
* `0` 이면 머리글을 안 세운다(글자 없는 제목은 0.235.0 부터 애초에 안 선다). */
|
|
12
|
+
titleLevel?: 0 | 2 | 3 | 4 | 5 | 6;
|
|
5
13
|
title?: ReactNode;
|
|
6
14
|
/**
|
|
7
15
|
* 제목 앞에 서는 lucide 아이콘 이름. Section 의 `icon` 과 같은 자리다.
|
|
@@ -54,7 +62,7 @@ interface PanelProps {
|
|
|
54
62
|
empty?: ReactNode;
|
|
55
63
|
children?: ReactNode;
|
|
56
64
|
}
|
|
57
|
-
export default function Panel({ title, icon, disc, actions, size, minHeight, maxWidth, className, scrollable, scrollbarGutter, compactRows, useBorder, useBg, useRowBorder, labelWidth, form, loading, empty, children }: PanelProps): import("react").JSX.Element;
|
|
65
|
+
export default function Panel({ title, titleLevel, icon, disc, actions, size, minHeight, maxWidth, className, scrollable, scrollbarGutter, compactRows, useBorder, useBg, useRowBorder, labelWidth, form, loading, empty, children }: PanelProps): import("react").JSX.Element;
|
|
58
66
|
/** @public */
|
|
59
67
|
export declare function PanelGrid({ cols, children }: {
|
|
60
68
|
cols?: number;
|
|
@@ -5,7 +5,7 @@ import { PanelRow } from './PanelRow';
|
|
|
5
5
|
import { Children } from 'react';
|
|
6
6
|
import Icon from './Icon';
|
|
7
7
|
import './Panel.css';
|
|
8
|
-
export default function Panel({ title, icon, disc, actions, size = 'medium', minHeight, maxWidth, className, scrollable, scrollbarGutter = 'auto', compactRows = false, useBorder = true, useBg = true, useRowBorder = true, labelWidth, form = false, loading, empty, children }) {
|
|
8
|
+
export default function Panel({ title, titleLevel = 2, icon, disc, actions, size = 'medium', minHeight, maxWidth, className, scrollable, scrollbarGutter = 'auto', compactRows = false, useBorder = true, useBg = true, useRowBorder = true, labelWidth, form = false, loading, empty, children }) {
|
|
9
9
|
const cls = cx('panel', `panel--${size}`, form ? 'panel--form' : '',
|
|
10
10
|
/* minHeight 패널은 마지막 행 아래 빈 공간이 생길 수 있으므로 행 하단 구분선 사용 */
|
|
11
11
|
minHeight !== undefined ? 'panel--fill' : '', !useBorder ? 'panel--no-border' : '', !useBg ? 'panel--no-bg' : '', !useRowBorder ? 'panel--no-row-border' : '', compactRows ? 'panel--compact-rows' : '', className);
|
|
@@ -16,7 +16,8 @@ export default function Panel({ title, icon, disc, actions, size = 'medium', min
|
|
|
16
16
|
style.maxWidth = typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth;
|
|
17
17
|
if (labelWidth !== undefined)
|
|
18
18
|
style['--panel-label-width'] = typeof labelWidth === 'number' ? `${labelWidth}px` : labelWidth;
|
|
19
|
-
|
|
19
|
+
const 제목태그 = titleLevel ? `h${titleLevel}` : 'div';
|
|
20
|
+
return (_jsxs("div", { className: cls, style: Object.keys(style).length ? style : undefined, children: [(!!title || disc != null) && (_jsxs("div", { className: cx('panel__header', disc != null ? 'panel__header--with-disc' : ''), children: [icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "panel__icon" })), _jsxs("div", { className: "panel__title-group", children: [!!title && (_jsx(제목태그, { className: `panel__title ${disc != null ? 'body-3b' : 'h5b'}`, children: title })), disc != null && _jsx("p", { className: "panel__disc caption-1", children: disc })] }), actions && _jsx("div", { className: "panel__actions", children: actions })] })), loading ? (_jsx(PanelRow, { variant: "loading" })
|
|
20
21
|
/* 비었는지는 React 가 그리는 것으로 센다 — `{조건 && 행들}` 이 false 면 children 이 false 라 빈 상태 문구가 안 떴다 */
|
|
21
22
|
) : empty != null && Children.toArray(children).length === 0 ? (_jsx(PanelRow, { variant: "empty", children: empty })) : scrollable ? (_jsx("div", { className: cx('panel__body--scroll', scrollbarGutter === 'stable' && 'panel__body--gutter-stable'), children: children })) : children] }));
|
|
22
23
|
}
|
|
@@ -99,7 +99,12 @@ export type PanelRowInput = Base & {
|
|
|
99
99
|
disabled?: boolean;
|
|
100
100
|
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
|
|
101
101
|
autoComplete?: string;
|
|
102
|
-
|
|
102
|
+
/** 칸 아래 안내 한 줄 — `aria-describedby` 로 **칸과 이어진다**(말귀 요청 (60)①, 2026-10-02).
|
|
103
|
+
*
|
|
104
|
+
* `ReactNode` 다 — 앱이 `<span role="status">` 로 감싸 **값에 따라 바뀌는 안내**를
|
|
105
|
+
* 그 자리에서 알릴 수 있게 한 것이다. `error` 로 보내면 될 일이 아니다: `error` 는
|
|
106
|
+
* `aria-invalid` 까지 세워 "틀린 값" 이라고 말하는데, 안내는 틀렸다는 말이 아니다. */
|
|
107
|
+
helperText?: ReactNode;
|
|
103
108
|
/** 최대 글자 수 — 설정 시 입력 제한 + 박스 안쪽 글자수 카운터 표시 */
|
|
104
109
|
maxLength?: number;
|
|
105
110
|
/** 글자수 카운터 표시 여부 (maxLength 설정 시, 기본 true) */
|
|
@@ -74,6 +74,8 @@ export function PanelRow(props) {
|
|
|
74
74
|
const toggleId = variant === 'toggle' ? `panel-toggle-${uid}` : undefined;
|
|
75
75
|
// 오류 문구 id — 드롭다운 행은 트리거가 이 문구를 설명으로 잇는다(aria-invalid 만으로는 무엇이 틀렸는지 안 들린다)
|
|
76
76
|
const errorId = error ? `panel-error-${uid}` : undefined;
|
|
77
|
+
/* 도움말은 variant 와 무관하게 한 자리에서 짓는다 — id 는 그릴 때와 걸 때 **같아야** 한다 */
|
|
78
|
+
const helperId = `panel-helper-${uid}`;
|
|
77
79
|
if (variant === 'toggle') {
|
|
78
80
|
const p = props;
|
|
79
81
|
right = _jsx(Toggle, { checked: p.checked, onChange: p.onChange, id: toggleId });
|
|
@@ -111,7 +113,7 @@ export function PanelRow(props) {
|
|
|
111
113
|
}
|
|
112
114
|
else if (variant === 'input') {
|
|
113
115
|
const p = props;
|
|
114
|
-
right = (_jsxs("div", { className: `panel-row__input-wrap${useFullWidth ? ' panel-row__input-wrap--full' : ''}`, children: [_jsx(TextField, { value: p.value, onChange: p.onChange ?? noop, onKeyDown: p.onKeyDown, onEnter: p.onEnter, placeholder: p.placeholder, ghostText: p.ghostText, ghostHint: p.ghostHint, inputMode: p.inputMode, readOnly: p.readOnly, disabled: p.disabled, type: p.type, autoComplete: p.autoComplete, maxLength: p.maxLength, showCount: p.showCount, multiline: p.multiline, rows: p.rows, fullWidth: useFullWidth, error: error, ariaLabel: label, required: required }), p.helperText && _jsx("div", { className: "panel-row__helper caption-1", children: p.helperText })] }));
|
|
116
|
+
right = (_jsxs("div", { className: `panel-row__input-wrap${useFullWidth ? ' panel-row__input-wrap--full' : ''}`, children: [_jsx(TextField, { value: p.value, onChange: p.onChange ?? noop, onKeyDown: p.onKeyDown, onEnter: p.onEnter, placeholder: p.placeholder, ghostText: p.ghostText, ghostHint: p.ghostHint, inputMode: p.inputMode, readOnly: p.readOnly, disabled: p.disabled, type: p.type, autoComplete: p.autoComplete, maxLength: p.maxLength, showCount: p.showCount, multiline: p.multiline, rows: p.rows, fullWidth: useFullWidth, error: error, ariaLabel: label, required: required, ariaDescribedBy: p.helperText ? helperId : undefined }), p.helperText && _jsx("div", { id: helperId, className: "panel-row__helper caption-1", children: p.helperText })] }));
|
|
115
117
|
}
|
|
116
118
|
else if (variant === 'custom') {
|
|
117
119
|
const p = props;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import './RingChart.css';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
-
import type { ChartLegendItem } from './ChartSection';
|
|
3
|
+
import type { ChartLegendItem, ChartSectionProps } from './ChartSection';
|
|
4
4
|
/** 시맨틱 사이즈 → 지름(px)·링 두께(px) 프리셋 */
|
|
5
5
|
export type RingChartSize = 'small' | 'medium' | 'large';
|
|
6
6
|
/** @public */
|
|
@@ -44,6 +44,8 @@ export interface RingChartProps {
|
|
|
44
44
|
* 감싼 카드로 렌더. 미지정 시 헤더 없는 순수 링만 렌더(기존 동작과 동일).
|
|
45
45
|
*/
|
|
46
46
|
titleKey?: string;
|
|
47
|
+
/** 제목 머리글 급 — 그대로 `ChartSection` 에 넘긴다(`titleKey` 지정 시에만). `BarChartProps.titleLevel` 과 같은 뜻 */
|
|
48
|
+
titleLevel?: ChartSectionProps['titleLevel'];
|
|
47
49
|
/** 헤더 우측 영역 — 기간 네비게이션, 상세보기 버튼 등 (titleKey 지정 시에만 사용) */
|
|
48
50
|
headRight?: ReactNode;
|
|
49
51
|
/** 제목 옆 아이콘(옅게 표시, StatChart와 동일한 톤) — 지정 시 headRight 자리에 렌더(titleKey 지정 시에만 사용) */
|
|
@@ -73,4 +75,4 @@ export interface RingChartProps {
|
|
|
73
75
|
* 미지정 시에는 헤더 없는 순수 링만 렌더된다(크기/색상 데모 등).
|
|
74
76
|
* @public
|
|
75
77
|
*/
|
|
76
|
-
export default function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid, onClick, ariaLabel, tooltip, ...ringProps }: RingChartProps): import("react").JSX.Element;
|
|
78
|
+
export default function RingChart({ titleKey, titleLevel, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid, onClick, ariaLabel, tooltip, ...ringProps }: RingChartProps): import("react").JSX.Element;
|
|
@@ -51,7 +51,7 @@ function RingChartCanvas({ done, total, segments, color = 'var(--color-main)', s
|
|
|
51
51
|
* 미지정 시에는 헤더 없는 순수 링만 렌더된다(크기/색상 데모 등).
|
|
52
52
|
* @public
|
|
53
53
|
*/
|
|
54
|
-
export default function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid = false, onClick, ariaLabel, tooltip, ...ringProps }) {
|
|
54
|
+
export default function RingChart({ titleKey, titleLevel, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid = false, onClick, ariaLabel, tooltip, ...ringProps }) {
|
|
55
55
|
const { t } = useTranslation();
|
|
56
56
|
if (!titleKey)
|
|
57
57
|
return _jsx(RingChartCanvas, { ...ringProps });
|
|
@@ -66,7 +66,7 @@ export default function RingChart({ titleKey, headRight, icon, showTotal, totalL
|
|
|
66
66
|
.filter(Boolean).join(' ') || undefined;
|
|
67
67
|
// StatChart(.manager-stat-card__icon)와 동일한 톤 — headRight 자리에 렌더(둘 중 하나만 사용)
|
|
68
68
|
const iconNode = icon && (_jsx("span", { className: "chart-section-icon", children: _jsx(Icon, { icon: icon, className: "chart-section-icon-inner" }) }));
|
|
69
|
-
return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, onClick: onClick, ariaLabel: ariaLabel, tooltip: tooltip, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: iconNode || (isSmall ? undefined : headRight), subRow: (showTotal || legend) && _jsxs(_Fragment, { children: [showTotal ? (_jsxs(ChartTotal, { label: !isSmall && totalLabelKey ? t(totalLabelKey) : undefined, children: [_jsx("span", { className: "barchart-total__num h0", children: pct }), _jsx("span", { className: "barchart-total__unit h5", children: "%" })] })) : _jsx("span", {}), legend && _jsx(ChartLegend, { items: legend })] }), children: loading ? (
|
|
69
|
+
return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, onClick: onClick, ariaLabel: ariaLabel, tooltip: tooltip, title: t(titleKey), titleLevel: titleLevel, titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: iconNode || (isSmall ? undefined : headRight), subRow: (showTotal || legend) && _jsxs(_Fragment, { children: [showTotal ? (_jsxs(ChartTotal, { label: !isSmall && totalLabelKey ? t(totalLabelKey) : undefined, children: [_jsx("span", { className: "barchart-total__num h0", children: pct }), _jsx("span", { className: "barchart-total__unit h5", children: "%" })] })) : _jsx("span", {}), legend && _jsx(ChartLegend, { items: legend })] }), children: loading ? (
|
|
70
70
|
/* 링도 같은 병이 있었다 — 기다림 상자 186px, 그려진 링은 지름(medium 120px)이다.
|
|
71
71
|
**그려질 링과 같은 자리·같은 지름**의 뼈대를 세우고 그 안에서 기다린다
|
|
72
72
|
(BarChart 와 같은 결, 사용자 지시 2026-09-17 "뼈대 먼저, 그 안에서 로딩"). */
|
|
@@ -17,6 +17,12 @@ export interface TextFieldProps {
|
|
|
17
17
|
label?: string;
|
|
18
18
|
/** 화면에 보이는 라벨 없이(placeholder만 쓸 때) 스크린리더용 이름을 지정. label이 있으면 무시됨 */
|
|
19
19
|
ariaLabel?: string;
|
|
20
|
+
/** **바깥에서 그린 설명**의 id — 이 칸 안의 `helperText`·`error` id 와 **함께** 걸린다(덮지 않는다).
|
|
21
|
+
*
|
|
22
|
+
* `PanelRow` 처럼 감싸는 쪽이 도움말을 제 자리에 그리는 자리가 있다. 전에는 그 글이 칸과
|
|
23
|
+
* 이어지지 않아, 보이기만 하고 낭독기에는 영 안 갔다(말귀 요청 (60)①, 2026-10-02).
|
|
24
|
+
* 빈칸 띄움으로 여러 개도 된다 — `aria-describedby` 는 id 목록이다. */
|
|
25
|
+
ariaDescribedBy?: string;
|
|
20
26
|
/** 필수 항목 표시 (*) */
|
|
21
27
|
required?: boolean;
|
|
22
28
|
/** placeholder 텍스트 */
|
|
@@ -13,7 +13,7 @@ import { useIsoLayoutEffect } from '../hooks/useIsoLayoutEffect';
|
|
|
13
13
|
import './TextField.css';
|
|
14
14
|
/** @public */
|
|
15
15
|
export default function TextField(props) {
|
|
16
|
-
const { value, defaultValue, name, inputRef, onChange, label, ariaLabel, required, placeholder, ghostText, ghostHint, type = 'text', disabled = false, readOnly = false, error, helperText, maxLength, size = 48, variant = 'input', fullWidth = false, newline = false, multiline = false, rows = 3, leftIcon, rightIcon, clearable = false, list, showCount = true, className = '', onFocus, onBlur, onKeyDown, onEnter, inputMode, autoComplete, autoFocus, suggestions: suggestionsProp, filterSuggestions, onSelectSuggestion, autoActiveFirst, openOnFocus = false, } = props;
|
|
16
|
+
const { value, defaultValue, name, inputRef, onChange, label, ariaLabel, ariaDescribedBy, required, placeholder, ghostText, ghostHint, type = 'text', disabled = false, readOnly = false, error, helperText, maxLength, size = 48, variant = 'input', fullWidth = false, newline = false, multiline = false, rows = 3, leftIcon, rightIcon, clearable = false, list, showCount = true, className = '', onFocus, onBlur, onKeyDown, onEnter, inputMode, autoComplete, autoFocus, suggestions: suggestionsProp, filterSuggestions, onSelectSuggestion, autoActiveFirst, openOnFocus = false, } = props;
|
|
17
17
|
// 읽기 전용·꺼진 칸은 후보로 값을 바꾸지 못한다 — 전에는 ↓·Enter·후보 누름으로 onChange 가 나갔다
|
|
18
18
|
const suggestions = readOnly || disabled ? undefined : suggestionsProp;
|
|
19
19
|
const { t } = useTranslation();
|
|
@@ -164,7 +164,9 @@ export default function TextField(props) {
|
|
|
164
164
|
'aria-label': !label && ariaLabel ? ariaLabel : undefined,
|
|
165
165
|
'aria-required': required,
|
|
166
166
|
'aria-invalid': !!error,
|
|
167
|
-
|
|
167
|
+
/* 안의 것(helperText·error)과 바깥의 것(ariaDescribedBy)을 **잇는다** — 둘 중 하나로
|
|
168
|
+
덮으면 오류가 뜬 순간 바깥 도움말이 사라지거나, 반대로 오류가 안 읽힌다. */
|
|
169
|
+
'aria-describedby': [(helperText || error) ? helperId : null, ariaDescribedBy].filter(Boolean).join(' ') || undefined,
|
|
168
170
|
// aria-expanded·aria-controls·aria-activedescendant 는 콤보박스 속성이라 role 없이는
|
|
169
171
|
// 스크린리더가 그냥 입력칸으로 읽고 "후보 n개"를 알리지 않는다 — 역할을 함께 준다.
|
|
170
172
|
role: suggestions ? 'combobox' : undefined,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
3
4
|
import { cx } from '../../utils/cx';
|
|
4
5
|
import { useCallback, useRef, useState, useEffect, useLayoutEffect, useMemo } from "react";
|
|
5
6
|
import InfoTooltip from "../Tooltip/InfoTooltip";
|
|
@@ -7,6 +8,7 @@ import Icon from '../Icon';
|
|
|
7
8
|
import "./CategoryIcon.css";
|
|
8
9
|
/** @public */
|
|
9
10
|
export default function CategoryIcon({ icon, label, tooltip, wrapperStyle, color, bgColor, useHover = true, iconSize = 'medium' }) {
|
|
11
|
+
const { t } = useTranslation();
|
|
10
12
|
const isSmallIcon = iconSize === 'small';
|
|
11
13
|
const cls = cx('category-icon', isSmallIcon ? 'category-icon--small' : '');
|
|
12
14
|
const svgSize = isSmallIcon ? 16 : 20;
|
|
@@ -75,5 +77,5 @@ export default function CategoryIcon({ icon, label, tooltip, wrapperStyle, color
|
|
|
75
77
|
setHover(v => !v);
|
|
76
78
|
}
|
|
77
79
|
}, []);
|
|
78
|
-
return (_jsxs("span", { ref: ref, className: "category-icon__wrap", onPointerEnter: handleHoverOn, onPointerLeave: handleHoverOff, onFocus: handleHoverOn, onBlur: handleHoverOff, onKeyDown: handleKeyDown, role: "button", tabIndex: 0, "aria-label": typeof tipText === 'string' ? tipText : undefined, "aria-haspopup": !!tipText, "aria-expanded": hover, "data-tooltip-ignore": true, children: [_jsx(Icon, { icon: icon, label: label, className: cls, imgClassName: "category-icon__image", maskClassName: "category-icon__mask", svgClassName: "category-icon__svg", wrapperStyle: resolvedWrapperStyle, size: svgSize }), hover && tipText && (_jsx(InfoTooltip, { anchorRect: anchorRect, text: typeof tipText === 'string' ? tipText : undefined, offset: 8, onClose: handleTooltipClose, className: "category-icon__tooltip", ariaLabel: typeof tipText === 'string' ? tipText : undefined }))] }));
|
|
80
|
+
return (_jsxs("span", { ref: ref, className: "category-icon__wrap", onPointerEnter: handleHoverOn, onPointerLeave: handleHoverOff, onFocus: handleHoverOn, onBlur: handleHoverOff, onKeyDown: handleKeyDown, role: "button", tabIndex: 0, "aria-label": typeof tipText === 'string' ? t('common.descriptionFor', { name: tipText, defaultValue: `${tipText} 설명` }) : undefined, "aria-haspopup": !!tipText, "aria-expanded": hover, "data-tooltip-ignore": true, children: [_jsx(Icon, { icon: icon, label: label, className: cls, imgClassName: "category-icon__image", maskClassName: "category-icon__mask", svgClassName: "category-icon__svg", wrapperStyle: resolvedWrapperStyle, size: svgSize }), hover && tipText && (_jsx(InfoTooltip, { anchorRect: anchorRect, text: typeof tipText === 'string' ? tipText : undefined, offset: 8, onClose: handleTooltipClose, className: "category-icon__tooltip", ariaLabel: typeof tipText === 'string' ? tipText : undefined }))] }));
|
|
79
81
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { ChartHeadSegment } from './ChartSection';
|
|
2
|
+
import type { ChartHeadSegment, ChartSectionProps } from './ChartSection';
|
|
3
3
|
import type { MonthBarData } from './barChartData';
|
|
4
4
|
/** @public */
|
|
5
5
|
export interface BarChartControl {
|
|
@@ -61,6 +61,12 @@ export interface BarChartProps {
|
|
|
61
61
|
barWidth?: number;
|
|
62
62
|
/** 섹션 제목 i18n 키 */
|
|
63
63
|
titleKey: string;
|
|
64
|
+
/** 제목을 머리글 몇 급으로 그릴지 — 그대로 `ChartSection` 에 넘긴다(말귀 요청 (60)③, 2026-10-02).
|
|
65
|
+
*
|
|
66
|
+
* `ChartSection` 은 (51) 에서 급을 열었는데 **막대·링은 그 문을 안 달았다.** 그래서 칸을
|
|
67
|
+
* 부르는 쪽이 급을 못 정하고 늘 기본 3 이 나갔다 — 대시보드 제목이 `h1` 이면 층이
|
|
68
|
+
* h1 → h3 로 한 급 비어 읽힌다. 자세한 뜻은 `ChartSectionProps.titleLevel` 참고. */
|
|
69
|
+
titleLevel?: ChartSectionProps['titleLevel'];
|
|
64
70
|
/** 구간(월) 키 → 주별/일별 데이터 */
|
|
65
71
|
data: Record<string, MonthBarData>;
|
|
66
72
|
colorA: string;
|