@ihabdevteam/core 0.247.0 → 0.249.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 +94 -0
- package/dist/components.cjs.js +179 -179
- package/dist/components.esm.css +3 -3
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +184 -184
- package/dist/ihabdevteam-core.d.ts +23 -4
- package/dist/packages/core/src/components/Alert.css +5 -2
- 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/BottomNav.js +8 -2
- package/dist/packages/core/src/components/Calendar.js +10 -3
- package/dist/packages/core/src/components/FileUpload.css +9 -0
- package/dist/packages/core/src/components/Header.js +9 -2
- package/dist/packages/core/src/components/NoticeBox.css +10 -1
- 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/Tooltip/DebugTooltip.css +1 -0
- package/dist/packages/core/src/components/Training/TrainingCard.css +16 -0
- package/dist/packages/core/src/components/barChartTypes.d.ts +7 -1
- package/dist/packages/core/src/i18n/locales/en.json +2 -1
- package/dist/packages/core/src/i18n/locales/ko.json +2 -1
- 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.249.0";
|
|
1649
1655
|
|
|
1650
1656
|
/** @public */
|
|
1651
1657
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -4359,7 +4365,12 @@ declare type PanelRowInput = Base & {
|
|
|
4359
4365
|
disabled?: boolean;
|
|
4360
4366
|
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
|
|
4361
4367
|
autoComplete?: string;
|
|
4362
|
-
|
|
4368
|
+
/** 칸 아래 안내 한 줄 — `aria-describedby` 로 **칸과 이어진다**(말귀 요청 (60)①, 2026-10-02).
|
|
4369
|
+
*
|
|
4370
|
+
* `ReactNode` 다 — 앱이 `<span role="status">` 로 감싸 **값에 따라 바뀌는 안내**를
|
|
4371
|
+
* 그 자리에서 알릴 수 있게 한 것이다. `error` 로 보내면 될 일이 아니다: `error` 는
|
|
4372
|
+
* `aria-invalid` 까지 세워 "틀린 값" 이라고 말하는데, 안내는 틀렸다는 말이 아니다. */
|
|
4373
|
+
helperText?: ReactNode;
|
|
4363
4374
|
/** 최대 글자 수 — 설정 시 입력 제한 + 박스 안쪽 글자수 카운터 표시 */
|
|
4364
4375
|
maxLength?: number;
|
|
4365
4376
|
/** 글자수 카운터 표시 여부 (maxLength 설정 시, 기본 true) */
|
|
@@ -6067,7 +6078,7 @@ export declare function reviewTableName(original: string | ReviewSource, today:
|
|
|
6067
6078
|
* 미지정 시에는 헤더 없는 순수 링만 렌더된다(크기/색상 데모 등).
|
|
6068
6079
|
* @public
|
|
6069
6080
|
*/
|
|
6070
|
-
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;
|
|
6071
6082
|
|
|
6072
6083
|
/** @public */
|
|
6073
6084
|
export declare interface RingChartProps {
|
|
@@ -6104,6 +6115,8 @@ export declare interface RingChartProps {
|
|
|
6104
6115
|
* 감싼 카드로 렌더. 미지정 시 헤더 없는 순수 링만 렌더(기존 동작과 동일).
|
|
6105
6116
|
*/
|
|
6106
6117
|
titleKey?: string;
|
|
6118
|
+
/** 제목 머리글 급 — 그대로 `ChartSection` 에 넘긴다(`titleKey` 지정 시에만). `BarChartProps.titleLevel` 과 같은 뜻 */
|
|
6119
|
+
titleLevel?: ChartSectionProps['titleLevel'];
|
|
6107
6120
|
/** 헤더 우측 영역 — 기간 네비게이션, 상세보기 버튼 등 (titleKey 지정 시에만 사용) */
|
|
6108
6121
|
headRight?: ReactNode;
|
|
6109
6122
|
/** 제목 옆 아이콘(옅게 표시, StatChart와 동일한 톤) — 지정 시 headRight 자리에 렌더(titleKey 지정 시에만 사용) */
|
|
@@ -7588,6 +7601,12 @@ export declare interface TextFieldProps {
|
|
|
7588
7601
|
label?: string;
|
|
7589
7602
|
/** 화면에 보이는 라벨 없이(placeholder만 쓸 때) 스크린리더용 이름을 지정. label이 있으면 무시됨 */
|
|
7590
7603
|
ariaLabel?: string;
|
|
7604
|
+
/** **바깥에서 그린 설명**의 id — 이 칸 안의 `helperText`·`error` id 와 **함께** 걸린다(덮지 않는다).
|
|
7605
|
+
*
|
|
7606
|
+
* `PanelRow` 처럼 감싸는 쪽이 도움말을 제 자리에 그리는 자리가 있다. 전에는 그 글이 칸과
|
|
7607
|
+
* 이어지지 않아, 보이기만 하고 낭독기에는 영 안 갔다(말귀 요청 (60)①, 2026-10-02).
|
|
7608
|
+
* 빈칸 띄움으로 여러 개도 된다 — `aria-describedby` 는 id 목록이다. */
|
|
7609
|
+
ariaDescribedBy?: string;
|
|
7591
7610
|
/** 필수 항목 표시 (*) */
|
|
7592
7611
|
required?: boolean;
|
|
7593
7612
|
/** placeholder 텍스트 */
|
|
@@ -41,13 +41,16 @@
|
|
|
41
41
|
color: var(--color-darkGray);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/* **같은 집안의 또 한 자리다**((61) 을 고치며 셈이 찾았다, 2026-10-02). 20 x 20px 으로
|
|
45
|
+
2.5.8 의 24 에 4px 모자랐다. 글자(x)는 18px 에 line-height 1 이라 **보이는 모양은
|
|
46
|
+
안 바뀐다** — 투명한 상자만 24 가 된다. 줄 폭이 4px 늘지만 알림은 그만큼 여유가 있다. */
|
|
44
47
|
.alert__dismiss {
|
|
45
48
|
flex-shrink: 0;
|
|
46
49
|
display: inline-flex;
|
|
47
50
|
align-items: center;
|
|
48
51
|
justify-content: center;
|
|
49
|
-
width:
|
|
50
|
-
height:
|
|
52
|
+
width: 24px;
|
|
53
|
+
height: 24px;
|
|
51
54
|
border: none;
|
|
52
55
|
background: transparent;
|
|
53
56
|
color: var(--color-gray);
|
|
@@ -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로 분리
|
|
@@ -85,8 +85,14 @@ export default function BottomNav({ items = [], activeKey, onChange, appMode, on
|
|
|
85
85
|
/* 기기를 돌리면(가로↔세로) 옛 좌표에 남아 메뉴가 엉뚱한 곳·화면 밖에 떴다 — 다시 잰다 */
|
|
86
86
|
onResize: 'reposition',
|
|
87
87
|
});
|
|
88
|
-
/* 단추 이름에 개수를 붙인다 — 배지는 aria-hidden 이라 개수는 이름으로만 전해진다.
|
|
88
|
+
/* 단추 이름에 개수를 붙인다 — 배지는 aria-hidden 이라 개수는 이름으로만 전해진다.
|
|
89
|
+
**그 칸의 수일 때만** 쓴다(`알림, 3개`). 닫힌 메뉴처럼 수가 남의 것이면 아래 `안에든수` 를 쓴다. */
|
|
89
90
|
const nameWithCount = useCallback((label, count) => count > 0 ? t('nav.badgeLabel', { label, count, defaultValue: '{{label}}, {{count}}개' }) : label, [t]);
|
|
91
|
+
/* **같은 자리가 하나 더 있었다**((62) 를 고치며 셈이 찾았다, 2026-10-02). `…` 단추의 수는
|
|
92
|
+
넘친 항목·유틸 항목의 배지를 **합한** 것이라 그 단추의 수가 아니다 — `더보기, 3개` 는
|
|
93
|
+
"더보기에 3개가 들어 있다" 로 읽히는데, 안에 든 항목 수와도 다르다. `activeMore` 가
|
|
94
|
+
있을 때는 더 어긋났다: `설정, 3개` 의 3 은 알림의 것이었다. */
|
|
95
|
+
const 안에든수 = useCallback((label, count) => count > 0 ? t('nav.badgeInsideLabel', { label, count, defaultValue: '{{label}}, 안에 알림 {{count}}개' }) : label, [t]);
|
|
90
96
|
const handleModeChange = useCallback(() => onAppModeChange?.(nextMode), [onAppModeChange, nextMode]);
|
|
91
97
|
// 넘친 nav 항목 또는 유틸 항목이 현재 활성 상태면 … 버튼에 그 아이콘/선택표시를 반영
|
|
92
98
|
const activeMore = useMemo(() => hiddenItems.find(it => !!it.key && it.key === activeKey) ?? moreItems.find(it => it.selected || (it.key && it.key === activeKey)), [hiddenItems, activeKey, moreItems]);
|
|
@@ -134,5 +140,5 @@ export default function BottomNav({ items = [], activeKey, onChange, appMode, on
|
|
|
134
140
|
onChange?.(it.key);
|
|
135
141
|
scrollNavTargetToTop(scrollContainer, 'BottomNav');
|
|
136
142
|
} } }), count > 0 && (_jsx("span", { "aria-hidden": "true", children: _jsx(Badge, { type: "dot", position: "top-right", number: Math.min(count, 99), size: 24 }) }))] }, it.key ?? `__slot-${i}`));
|
|
137
|
-
}) }), showMore && (_jsxs(_Fragment, { children: [_jsx("div", { className: "bottom-nav__divider", "aria-hidden": "true" }), _jsx("div", { className: "bottom-nav__right", children: _jsxs("div", { ref: moreRef, className: "bottom-nav__slot", children: [_jsx(Button, { htmlType: "button", size: 52, type: "dot", icon: activeMore?.icon ?? 'ellipsis', ariaLabel:
|
|
143
|
+
}) }), showMore && (_jsxs(_Fragment, { children: [_jsx("div", { className: "bottom-nav__divider", "aria-hidden": "true" }), _jsx("div", { className: "bottom-nav__right", children: _jsxs("div", { ref: moreRef, className: "bottom-nav__slot", children: [_jsx(Button, { htmlType: "button", size: 52, type: "dot", icon: activeMore?.icon ?? 'ellipsis', ariaLabel: 안에든수(activeMore?.label ?? t('nav.more', '더보기'), moreBadge), selected: !!activeMore, className: "bottom-nav-item", onClick: handleToggleMore, "aria-expanded": moreOpen, "aria-controls": moreOpen ? moreMenuId : undefined }), moreBadge > 0 && (_jsx("span", { "aria-hidden": "true", children: _jsx(Badge, { type: "dot", position: "top-right", number: Math.min(moreBadge, 99), size: 24 }) })), moreOpen && createPortal(_jsx("div", { id: moreMenuId, role: "group", "aria-label": t('nav.moreMenu', '더보기 메뉴'), className: "tooltip-content", style: menuStyle, ref: menuRef, children: _jsx(MenuTooltip, { columns: moreColumns, onClose: handleCloseMore }) }), document.body)] }) })] }))] }));
|
|
138
144
|
}
|
|
@@ -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'
|
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
color: var(--color-main);
|
|
56
56
|
padding: var(--spacing-xs) var(--spacing-m);
|
|
57
57
|
cursor: pointer;
|
|
58
|
+
/* 안쪽 여백(8px)만으로도 이미 24 를 넘지만, **바닥을 적어 둔다** — 글꼴이 작아지면
|
|
59
|
+
조용히 24 아래로 내려가는 자리다(`test/rawButtonHitArea.test.ts` 가 이것을 묻는다). */
|
|
60
|
+
min-height: 24px;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
.file-upload__browse-button:disabled {
|
|
@@ -97,6 +100,10 @@
|
|
|
97
100
|
color: var(--color-darkGray);
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
/* 안에 든 것이 `<Icon size={14} />` 하나이고 여백이 없어 **14 x 14px** 이었다
|
|
104
|
+
((61) 을 고치며 셈이 찾았다, 2026-10-02). 파일 목록은 줄마다 이 단추 하나뿐이라
|
|
105
|
+
옆으로 빗나가도 다른 단추를 누르지는 않지만, 2.5.8 의 24 에 한참 못 미친다.
|
|
106
|
+
아이콘은 그대로 14px — 투명한 상자만 24 가 된다. */
|
|
100
107
|
.file-upload__remove-button {
|
|
101
108
|
appearance: none;
|
|
102
109
|
border: none;
|
|
@@ -107,6 +114,8 @@
|
|
|
107
114
|
color: var(--color-gray);
|
|
108
115
|
cursor: pointer;
|
|
109
116
|
flex-shrink: 0;
|
|
117
|
+
min-width: 24px;
|
|
118
|
+
min-height: 24px;
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
.file-upload__remove-button:hover {
|
|
@@ -124,8 +124,15 @@ export default function Header({ activeKey, onChange, items, malgwiItems: _malgw
|
|
|
124
124
|
onClick: () => { it.onClick?.(); closeUtilMenu(); },
|
|
125
125
|
})),
|
|
126
126
|
}], [bottomMoreItems, activeKey, closeUtilMenu]);
|
|
127
|
-
return (_jsxs("aside", { className: `header${appMode === 'manager' ? ' header--manager' : ''}`, children: [_jsxs("div", { className: "header__bi", children: [_jsx(BrandIdentity, { appMode: appMode }), _jsxs("div", { className: "header__menu-button", ref: utilTriggerRef, children: [_jsx(Button, { htmlType: "button", size: 48, type: "dot", shape: "pill", variant: "default", useBorder: false, icon: 'menu',
|
|
128
|
-
|
|
127
|
+
return (_jsxs("aside", { className: `header${appMode === 'manager' ? ' header--manager' : ''}`, children: [_jsxs("div", { className: "header__bi", children: [_jsx(BrandIdentity, { appMode: appMode }), _jsxs("div", { className: "header__menu-button", ref: utilTriggerRef, children: [_jsx(Button, { htmlType: "button", size: 48, type: "dot", shape: "pill", variant: "default", useBorder: false, icon: 'menu',
|
|
128
|
+
/* **수가 이 단추의 것이 아니다**(말귀 요청 (62), 2026-10-02). `nav.badgeLabel` 은
|
|
129
|
+
"칸 이름 + 그 칸의 수" 라는 뜻이고, `SideNav`·`BottomNav`·`MenuTooltip` 셋은
|
|
130
|
+
`label` 이 그 수를 가진 칸의 이름이라 `알림, 3개` 로 말이 된다. 이 단추만
|
|
131
|
+
`label` 이 `메뉴 열기` 여서 "메뉴에 3개가 들어 있다" 로 읽혔다 — 열면 다섯이다.
|
|
132
|
+
그래서 **닫힌 것 안에 든 수**를 말하는 열쇠를 따로 쓴다(`nav.badgeLabel` 은
|
|
133
|
+
손대지 않는다 — 거기에 다른 뜻을 겹치면 맞는 세 쓰임이 서로를 막는다). */
|
|
134
|
+
ariaLabel: onNotification && unreadCount && unreadCount > 0
|
|
135
|
+
? t('nav.badgeInsideLabel', { label: t('header.menuOpen', '메뉴 열기'), count: unreadCount, defaultValue: '{{label}}, 안에 알림 {{count}}개' })
|
|
129
136
|
: t('header.menuOpen', '메뉴 열기'), onClick: toggleUtilMenu, "aria-expanded": utilOpen, "aria-controls": utilOpen ? utilMenuId : undefined }), !!onNotification && !!unreadCount && unreadCount > 0 && (_jsx(Badge, { type: "dot", number: Math.min(unreadCount, 99), size: 24, "aria-hidden": true })), utilOpen && createPortal(_jsx("div", { id: utilMenuId, role: "group", "aria-label": t('header.utilMenu', '유틸리티 메뉴'), className: "tooltip-content", style: utilMenuStyle, ref: utilMenuRef, children: _jsx(MenuTooltip, { columns: utilColumns, onClose: closeUtilMenu }) }), document.body)] })] }), _jsx(SideNav, { scrollContainer: scrollContainer, items: items, activeKey: activeKey ?? undefined, onChange: onChange, unreadCount: unreadCount, onDebug: onDebug, onNotification: onNotification, onAccount: onAccount, onSetting: onSetting, onHelp: onHelp, onFeedback: onFeedback, appMode: appMode, onAppModeChange: onAppModeChange }), isMobile && _jsx(BottomNav, { items: bottomItems, activeKey: activeKey ?? undefined, onChange: handleBottomChange, scrollContainer: scrollContainer, appMode: appMode, onAppModeChange: onAppModeChange })] }));
|
|
130
137
|
}
|
|
131
138
|
/**
|
|
@@ -62,7 +62,16 @@
|
|
|
62
62
|
line-height: 1;
|
|
63
63
|
color: var(--color-lightGray);
|
|
64
64
|
cursor: pointer;
|
|
65
|
-
|
|
65
|
+
/* 18px 글자에 위아래 2px 뿐이라 **22px** 이었다 — 2.5.8 의 24 에 2px 모자랐다
|
|
66
|
+
((61) 을 고치며 셈이 찾았다, 2026-10-02). 보이는 것은 x 하나이고 배경이 없어,
|
|
67
|
+
상자만 24 로 키우면 눈에는 아무 변화가 없다. `top`·`right` 가 모서리를 잡으므로
|
|
68
|
+
자리도 안 밀린다. */
|
|
69
|
+
min-width: 24px;
|
|
70
|
+
min-height: 24px;
|
|
71
|
+
display: inline-flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
padding: 0;
|
|
66
75
|
border-radius: var(--radius-xs);
|
|
67
76
|
transition: color 0.12s;
|
|
68
77
|
}
|
|
@@ -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,
|
|
@@ -75,10 +75,26 @@
|
|
|
75
75
|
margin-right: -8px;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/* 게시자 이름은 **글자 높이가 곧 누름감 높이**였다 — 320px 라이브 실측 29 x 13px
|
|
79
|
+
(말귀 요청 (61), 2026-10-02). WCAG 2.5.8(AA) 의 24x24 에 못 미치는데, 여기서는
|
|
80
|
+
**"사이가 떨어져 있으면 봐준다" 예외를 못 쓴다**: 빗나간 손가락이 빈 데가 아니라
|
|
81
|
+
260px 짜리 카드(`role="button"`)에 닿아 **게시자를 보려다 훈련 세트 상세가 열린다.**
|
|
82
|
+
|
|
83
|
+
`.top-banner__dot` 이 쓰는 투명 `::after` 는 여기서 못 쓴다 — 이 단추는 말줄임을
|
|
84
|
+
위해 `overflow: hidden` 이라 상자 밖으로 뻗은 의사요소가 **잘린다.** 그래서 안쪽
|
|
85
|
+
여백으로 넓히고 같은 값의 음수 바깥여백으로 되돌린다 — **보이는 자리와 자리차지는
|
|
86
|
+
그대로이고 누름감만 넓어진다**(카드 밑줄 간격이 안 밀린다).
|
|
87
|
+
|
|
88
|
+
**가로는 안 넓힌다.** 옆에 선 것(`.training-card__date`)은 누를 것이 아니라 그냥
|
|
89
|
+
글자라, 가로 방향은 2.5.8 의 간격 예외가 실제로 덮는다. 넓히면 날짜 글자 왼쪽을
|
|
90
|
+
눌러도 게시자가 열려 더 어긋난다. 막힌 쪽은 **세로** 였다. */
|
|
78
91
|
.training-card__publisher {
|
|
79
92
|
background: none;
|
|
80
93
|
border: none;
|
|
81
94
|
padding: 0;
|
|
95
|
+
min-height: 24px;
|
|
96
|
+
padding-block: 6px;
|
|
97
|
+
margin-block: -6px;
|
|
82
98
|
color: var(--color-gray);
|
|
83
99
|
cursor: pointer;
|
|
84
100
|
font-size: 11px;
|
|
@@ -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;
|
|
@@ -191,7 +191,8 @@
|
|
|
191
191
|
"company": "Company Info",
|
|
192
192
|
"more": "More",
|
|
193
193
|
"moreMenu": "More menu",
|
|
194
|
-
"badgeLabel": "{{label}}, {{count}}"
|
|
194
|
+
"badgeLabel": "{{label}}, {{count}}",
|
|
195
|
+
"badgeInsideLabel": "{{label}}, {{count}} notifications inside"
|
|
195
196
|
},
|
|
196
197
|
"settings": {
|
|
197
198
|
"language": {
|