@ihabdevteam/core 0.132.0 → 0.133.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 +65 -0
- package/dist/components.cjs.js +178 -178
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +182 -182
- package/dist/ihabdevteam-core.d.ts +1 -12
- package/dist/packages/core/src/components/BarChart.js +2 -2
- package/dist/packages/core/src/components/BottomNav.js +3 -3
- package/dist/packages/core/src/components/BulkBar.js +1 -1
- package/dist/packages/core/src/components/ChoiceChips.js +1 -1
- package/dist/packages/core/src/components/CodeBox.js +1 -1
- package/dist/packages/core/src/components/DatePicker.js +16 -3
- package/dist/packages/core/src/components/Dropdown.js +13 -3
- package/dist/packages/core/src/components/FileUpload.js +8 -7
- package/dist/packages/core/src/components/FilterPanel.js +8 -5
- package/dist/packages/core/src/components/FilterSectionInfo.js +1 -1
- package/dist/packages/core/src/components/Header.js +1 -1
- package/dist/packages/core/src/components/HearingDeviceInfo.js +1 -1
- package/dist/packages/core/src/components/PageTitle.js +1 -1
- package/dist/packages/core/src/components/Pagination.js +1 -1
- package/dist/packages/core/src/components/PanelRow.js +3 -3
- package/dist/packages/core/src/components/PaymentPlan.js +1 -1
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +2 -2
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +2 -2
- package/dist/packages/core/src/components/Popup/Popup.js +15 -3
- package/dist/packages/core/src/components/Popup/SaveTablePopup.js +1 -1
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +1 -1
- package/dist/packages/core/src/components/Popup/WordDataUpdatePopup.js +3 -3
- package/dist/packages/core/src/components/SegmentedControl.js +1 -1
- package/dist/packages/core/src/components/SideNav.js +1 -1
- package/dist/packages/core/src/components/Table.js +2 -2
- package/dist/packages/core/src/components/Tabs.js +7 -2
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +1 -1
- package/dist/packages/core/src/components/Tooltip/TextBoxTooltip.js +1 -1
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +13 -5
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingBlock.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +51 -23
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopupTitle.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +26 -5
- package/dist/packages/core/src/components/Training/TrainingStepConfirm.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +1 -1
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +11 -6
- package/dist/packages/core/src/components/Training/useAudioPreview.js +7 -3
- package/dist/packages/core/src/components/UpdateAlert.js +1 -1
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +2 -2
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +3 -3
- package/dist/packages/core/src/components/Word/WordTableBlock.js +2 -2
- package/dist/packages/core/src/components/Word/WordTableButton.js +1 -1
- package/dist/packages/core/src/components/WordTable.js +6 -6
- package/dist/packages/core/src/components/useToastLifecycle.js +8 -1
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +10 -9
- package/dist/packages/core/src/hooks/useDocumentTitle.d.ts +0 -11
- package/dist/packages/core/src/hooks/useDocumentTitle.js +16 -3
- package/dist/packages/core/src/hooks/useFocusTrap.js +6 -2
- package/dist/packages/core/src/utils/audioPeaks.js +13 -5
- package/dist/packages/core/src/utils/csv.js +3 -1
- package/dist/packages/core/src/utils/koreanSyllable.js +2 -1
- package/dist/packages/core/src/utils/textSort.d.ts +0 -10
- package/dist/packages/core/src/utils/textSort.js +7 -2
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +28 -4
- 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
|
@@ -1369,7 +1369,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1369
1369
|
*
|
|
1370
1370
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1371
1371
|
*/
|
|
1372
|
-
export declare const CORE_VERSION = "0.
|
|
1372
|
+
export declare const CORE_VERSION = "0.133.0";
|
|
1373
1373
|
|
|
1374
1374
|
/** @public */
|
|
1375
1375
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -7883,17 +7883,6 @@ export declare function useDeviceManagement(): {
|
|
|
7883
7883
|
handleSelectFromList: (form: DeviceForm) => void;
|
|
7884
7884
|
};
|
|
7885
7885
|
|
|
7886
|
-
/**
|
|
7887
|
-
* 현재 화면의 제목을 `document.title`에 반영한다 — `'<제목> · <앱 이름>'`.
|
|
7888
|
-
*
|
|
7889
|
-
* SPA는 라우트를 옮겨도 문서 제목이 그대로라, 브라우저 탭·히스토리·북마크로 위치를
|
|
7890
|
-
* 구분할 수 없고 화면낭독기도 이동을 인지하지 못한다(WCAG 2.4.2).
|
|
7891
|
-
* 언마운트하면 이전 제목으로 되돌린다.
|
|
7892
|
-
*
|
|
7893
|
-
* @param title 페이지 제목. 비어 있거나 문자열이 아니면 아무것도 하지 않는다.
|
|
7894
|
-
*
|
|
7895
|
-
* @public
|
|
7896
|
-
*/
|
|
7897
7886
|
export declare function useDocumentTitle(title?: string | null): void;
|
|
7898
7887
|
|
|
7899
7888
|
/**
|
|
@@ -97,7 +97,7 @@ export function BarChartControls({ control }) {
|
|
|
97
97
|
주 보기에서 낭독기·툴팁이 실제 동작과 다른 말을 한다(말귀 검수 실측). */
|
|
98
98
|
const prevLabel = period === 'week' ? t('calendar.prevWeek', '이전 주') : t('calendar.prevMonth', '이전 달');
|
|
99
99
|
const nextLabel = period === 'week' ? t('calendar.nextWeek', '다음 주') : t('calendar.nextMonth', '다음 달');
|
|
100
|
-
return (_jsxs("div", { className: "manager-section-controls", children: [_jsx(Button, { size: 32, type: "dot", icon: "chevron-left", ariaLabel: prevLabel, showTooltipInfo: prevLabel, disabled: activeIdx === activeOptions.length - 1, onClick: handlePrev }), _jsx(Dropdown, { value: range, onChange: setRange, options: activeOptions, size: 40, className: "month-dropdown", ariaLabel: t('calendar.rangeSelect', '기간 선택') }), _jsx(Button, { size: 32, type: "dot", icon: "chevron-right", ariaLabel: nextLabel, showTooltipInfo: nextLabel, disabled: activeIdx <= 0, onClick: handleNext }), _jsx(SegmentedControl, { ariaLabel: t('calendar.periodSelect', '기간 단위'), options: periodOptions, value: period, onChange: (k) => setPeriod(k), size: 32 })] }));
|
|
100
|
+
return (_jsxs("div", { className: "manager-section-controls", children: [_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "chevron-left", ariaLabel: prevLabel, showTooltipInfo: prevLabel, disabled: activeIdx === activeOptions.length - 1, onClick: handlePrev }), _jsx(Dropdown, { value: range, onChange: setRange, options: activeOptions, size: 40, className: "month-dropdown", ariaLabel: t('calendar.rangeSelect', '기간 선택') }), _jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "chevron-right", ariaLabel: nextLabel, showTooltipInfo: nextLabel, disabled: activeIdx <= 0, onClick: handleNext }), _jsx(SegmentedControl, { ariaLabel: t('calendar.periodSelect', '기간 단위'), options: periodOptions, value: period, onChange: (k) => setPeriod(k), size: 32 })] }));
|
|
101
101
|
}
|
|
102
102
|
/**
|
|
103
103
|
* 한 주(월요일부터 7일)의 날마다 — 그 날의 자료와 미래 여부. 달을 넘는 날은 그 달
|
|
@@ -239,7 +239,7 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
|
|
|
239
239
|
// size='large' — 카드가 가로 폭을 거의 100% 차지(1열)
|
|
240
240
|
const isLarge = size === 'large';
|
|
241
241
|
const sectionClassName = isSmall ? 'manager-section--compact' : isLarge ? 'manager-section--full' : undefined;
|
|
242
|
-
return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: isSmall ? undefined : _jsxs(_Fragment, { children: [!hideControls && _jsx(BarChartControls, { control: control }), onDetail && (_jsx("span", { className: "manager-bar-chart-detail-btn", children: _jsx(Button, { size: 32, type: "dot", icon: "list", ariaLabel: t('trainingStatus.detail', '상세보기'), onClick: onDetail }) }))] }), subRow: _jsxs(_Fragment, { children: [showTotal ? ((rate || avgMode || (!!totalLabelKey && !!totalFormat)) ? (
|
|
242
|
+
return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: isSmall ? undefined : _jsxs(_Fragment, { children: [!hideControls && _jsx(BarChartControls, { control: control }), onDetail && (_jsx("span", { className: "manager-bar-chart-detail-btn", children: _jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "list", ariaLabel: t('trainingStatus.detail', '상세보기'), onClick: onDetail }) }))] }), subRow: _jsxs(_Fragment, { children: [showTotal ? ((rate || avgMode || (!!totalLabelKey && !!totalFormat)) ? (
|
|
243
243
|
// 진행률/평균 모드 또는 totalLabelKey+totalFormat 조합: 라벨(윗줄) + 값(아랫줄). size='small'은 라벨 생략
|
|
244
244
|
_jsx(ChartTotal, { label: isSmall ? undefined : `${t(control.period === 'week' ? 'manager.chart.weeklyPrefix' : 'manager.chart.monthlyPrefix')} ${t(totalLabelKey ?? labelAKey)}`, children: totalFormat ? (
|
|
245
245
|
// 숫자는 h0, 단위(시간/분/초 등)는 h5로 분리
|
|
@@ -97,8 +97,8 @@ export default function BottomNav({ items = [], activeKey, onChange, appMode, on
|
|
|
97
97
|
}], [hiddenItems, moreItems, activeKey, onChange, handleCloseMore]);
|
|
98
98
|
// "…" 단추 배지 — 메뉴 안에서만 보이는 개수(넘친 항목 + 유틸)를 닫힌 채로도 알린다.
|
|
99
99
|
const moreBadge = [...hiddenItems, ...moreItems].reduce((sum, it) => sum + badgeOf(it.badgeCount), 0);
|
|
100
|
-
return (_jsxs("nav", { className: "bottom-nav", "aria-label": t('nav.bottomNav', '하단 네비게이션'), children: [onAppModeChange && (_jsxs(_Fragment, { children: [_jsx("div", { className: "bottom-nav__left", children: _jsx(Button, { size: 52, type: "dot", icon: modeIcon, ariaLabel: modeLabel, className: "bottom-nav-item", onClick: handleModeChange }) }), _jsx("div", { className: "bottom-nav__divider", "aria-hidden": "true" })] })), _jsx("div", { className: "bottom-nav__items", ref: itemsRef, children: visibleItems.map((it) => {
|
|
100
|
+
return (_jsxs("nav", { className: "bottom-nav", "aria-label": t('nav.bottomNav', '하단 네비게이션'), children: [onAppModeChange && (_jsxs(_Fragment, { children: [_jsx("div", { className: "bottom-nav__left", children: _jsx(Button, { htmlType: "button", size: 52, type: "dot", icon: modeIcon, ariaLabel: modeLabel, className: "bottom-nav-item", onClick: handleModeChange }) }), _jsx("div", { className: "bottom-nav__divider", "aria-hidden": "true" })] })), _jsx("div", { className: "bottom-nav__items", ref: itemsRef, children: visibleItems.map((it) => {
|
|
101
101
|
const count = badgeOf(it.badgeCount);
|
|
102
|
-
return (_jsxs("span", { className: "bottom-nav__slot", children: [_jsx(Button, { size: 52, type: "dot", icon: it.icon, ariaLabel: nameWithCount(it.label, count), selected: it.key === activeKey, disabled: it.state === 'disabled', className: "bottom-nav-item", onClick: () => it.key && onChange?.(it.key) }), count > 0 && (_jsx("span", { "aria-hidden": "true", children: _jsx(Badge, { type: "dot", position: "top-right", number: Math.min(count, 99), size: 24 }) }))] }, it.key));
|
|
103
|
-
}) }), 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, { size: 52, type: "dot", icon: activeMore?.icon ?? 'ellipsis', ariaLabel: nameWithCount(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)] }) })] }))] }));
|
|
102
|
+
return (_jsxs("span", { className: "bottom-nav__slot", children: [_jsx(Button, { htmlType: "button", size: 52, type: "dot", icon: it.icon, ariaLabel: nameWithCount(it.label, count), selected: it.key === activeKey, disabled: it.state === 'disabled', className: "bottom-nav-item", onClick: () => it.key && onChange?.(it.key) }), count > 0 && (_jsx("span", { "aria-hidden": "true", children: _jsx(Badge, { type: "dot", position: "top-right", number: Math.min(count, 99), size: 24 }) }))] }, it.key));
|
|
103
|
+
}) }), 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: nameWithCount(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)] }) })] }))] }));
|
|
104
104
|
}
|
|
@@ -43,5 +43,5 @@ export default function BulkBar({ count, actions, onClear, clearLabel: clearLabe
|
|
|
43
43
|
단추가 Tab 순서와 낭독기에 남아, 보이지 않는 '선택 해제'·'삭제' 에 포커스가 갔다.
|
|
44
44
|
inert 로 포커스·낭독기에서 함께 뺀다(보이는 모양은 CSS 그대로). aria-hidden 은
|
|
45
45
|
inert 를 모르는 오래된 브라우저를 위한 덧댐이다. */
|
|
46
|
-
inert: !show || undefined, "aria-hidden": show ? undefined : true, children: [_jsx("span", { ref: contentRef, className: "bulk-bar__count body-2", children: content ?? _jsx("b", { children: t('common.selectedCount', { count }) }) }), max !== undefined && (_jsx(Badge, { size: "small", fill: "soft", variant: "main", label: `${count} / ${max}` })), _jsxs("div", { className: `bulk-bar__actions${stacked ? ' bulk-bar__actions--stacked' : ''}`, children: [_jsx(Button, { size: 40, label: clearLabel, onClick: onClear }), actions.map((a, i) => (_jsx(Button, { size: 40, variant: a.variant ?? 'default', icon: a.icon, label: a.label, onClick: a.onClick }, i)))] })] }));
|
|
46
|
+
inert: !show || undefined, "aria-hidden": show ? undefined : true, children: [_jsx("span", { ref: contentRef, className: "bulk-bar__count body-2", children: content ?? _jsx("b", { children: t('common.selectedCount', { count }) }) }), max !== undefined && (_jsx(Badge, { size: "small", fill: "soft", variant: "main", label: `${count} / ${max}` })), _jsxs("div", { className: `bulk-bar__actions${stacked ? ' bulk-bar__actions--stacked' : ''}`, children: [_jsx(Button, { htmlType: "button", size: 40, label: clearLabel, onClick: onClear }), actions.map((a, i) => (_jsx(Button, { htmlType: "button", size: 40, variant: a.variant ?? 'default', icon: a.icon, label: a.label, onClick: a.onClick }, i)))] })] }));
|
|
47
47
|
}
|
|
@@ -4,5 +4,5 @@ import './ChoiceChips.css';
|
|
|
4
4
|
/** @public */
|
|
5
5
|
export default function ChoiceChips({ subs = [], selected, onSelect, size = 'medium' }) {
|
|
6
6
|
const buttonSize = size === 'small' ? 32 : 40;
|
|
7
|
-
return (_jsx("div", { className: `choice-chips choice-chips--${size}`, children: _jsx("div", { className: "choice-chips__list", children: subs.map((s) => (_jsx(Button, { size: buttonSize, type: "bar", label: s.label, selected: selected === s.key, className: "choice-chips__button", onClick: () => onSelect?.(s.key) }, s.key))) }) }));
|
|
7
|
+
return (_jsx("div", { className: `choice-chips choice-chips--${size}`, children: _jsx("div", { className: "choice-chips__list", children: subs.map((s) => (_jsx(Button, { htmlType: "button", size: buttonSize, type: "bar", label: s.label, selected: selected === s.key, className: "choice-chips__button", onClick: () => onSelect?.(s.key) }, s.key))) }) }));
|
|
8
8
|
}
|
|
@@ -128,5 +128,5 @@ export default function CodeBox({ children, language = 'bash', title = '', showC
|
|
|
128
128
|
};
|
|
129
129
|
const tokens = tokenize(codeText, language);
|
|
130
130
|
const tokenTypo = size === 'small' ? 'caption-1' : 'body-3';
|
|
131
|
-
return (_jsxs("div", { className: cx('code-box', size === 'small' ? 'code-box--small' : '', className), children: [(title || showCopy) && (_jsxs("div", { className: "code-box__header", role: "toolbar", "aria-label": t('common.codeBlock', '코드 블록'), children: [_jsx("div", { className: "code-box__title caption-1", children: title }), showCopy && (_jsx(Button, { className: "code-box__copy", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: copied ? 'check' : 'copy', onClick: handleCopy, ariaLabel: copied ? t('common.copied', '복사됨') : t('common.copyCode', '코드 복사') }))] })), _jsx("pre", { className: "code-box__inner", "data-lang": language, children: _jsx("code", { children: tokens.map((t, i) => (_jsx("span", { className: `code-token ${t.type} ${tokenTypo}`, children: t.content }, i))) }) })] }));
|
|
131
|
+
return (_jsxs("div", { className: cx('code-box', size === 'small' ? 'code-box--small' : '', className), children: [(title || showCopy) && (_jsxs("div", { className: "code-box__header", role: "toolbar", "aria-label": t('common.codeBlock', '코드 블록'), children: [_jsx("div", { className: "code-box__title caption-1", children: title }), showCopy && (_jsx(Button, { htmlType: "button", className: "code-box__copy", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: copied ? 'check' : 'copy', onClick: handleCopy, ariaLabel: copied ? t('common.copied', '복사됨') : t('common.copyCode', '코드 복사') }))] })), _jsx("pre", { className: "code-box__inner", "data-lang": language, children: _jsx("code", { children: tokens.map((t, i) => (_jsx("span", { className: `code-token ${t.type} ${tokenTypo}`, children: t.content }, i))) }) })] }));
|
|
132
132
|
}
|
|
@@ -95,8 +95,10 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
95
95
|
if (!open)
|
|
96
96
|
return;
|
|
97
97
|
const 판 = menuRef.current;
|
|
98
|
-
const 갈곳 = 판?.querySelector('.date-picker__day--selected') ??
|
|
99
|
-
판?.querySelector('.date-picker__day--today') ??
|
|
98
|
+
const 갈곳 = 판?.querySelector('.date-picker__day--selected:not(:disabled)') ??
|
|
99
|
+
판?.querySelector('.date-picker__day--today:not(:disabled)') ??
|
|
100
|
+
/* 오늘이 min/max 밖이라 꺼져 있으면 focus() 가 실패해 트리거에 남았다 — 켜진 첫 날로 */
|
|
101
|
+
판?.querySelector('.date-picker__day:not(:disabled)') ??
|
|
100
102
|
판?.querySelector(FOCUSABLE_SELECTOR) ??
|
|
101
103
|
판;
|
|
102
104
|
갈곳?.focus?.();
|
|
@@ -199,6 +201,17 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
199
201
|
return true;
|
|
200
202
|
return false;
|
|
201
203
|
};
|
|
204
|
+
/* Tab 을 받을 날 — 활성일이 min/max 밖이라 꺼져 있으면 켜진 첫 날로. 꺼진 단추는 Tab 을 못 받아
|
|
205
|
+
격자에 Tab 자리가 **하나도 없어** 키보드로 달력에 들어갈 수 없었다(값 없음 + min=내일). */
|
|
206
|
+
const 탭자리 = (() => {
|
|
207
|
+
const 끝 = daysInMonth(viewY, viewM);
|
|
208
|
+
if (활성일 >= 1 && 활성일 <= 끝 && !isDisabledDay(활성일))
|
|
209
|
+
return 활성일;
|
|
210
|
+
for (let d = 1; d <= 끝; d++)
|
|
211
|
+
if (!isDisabledDay(d))
|
|
212
|
+
return d;
|
|
213
|
+
return -1;
|
|
214
|
+
})();
|
|
202
215
|
const cells = [];
|
|
203
216
|
for (let i = 0; i < firstDay(viewY, viewM); i++)
|
|
204
217
|
cells.push(null);
|
|
@@ -218,7 +231,7 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
218
231
|
const isToday = iso === toISO(today.getFullYear(), today.getMonth(), today.getDate());
|
|
219
232
|
const dis = isDisabledDay(d);
|
|
220
233
|
const col = (i % 7);
|
|
221
|
-
return (_jsx("button", { type: "button", disabled: dis, "data-day": d, tabIndex: d ===
|
|
234
|
+
return (_jsx("button", { type: "button", disabled: dis, "data-day": d, tabIndex: d === 탭자리 ? 0 : -1, "aria-current": isToday ? 'date' : undefined, onClick: () => !dis && selectDay(d), className: cx('date-picker__day caption-1', isSelected && 'date-picker__day--selected', isToday && !isSelected && 'date-picker__day--today', dis && 'date-picker__day--disabled', col === 0 && 'date-picker__day--sun', col === 6 && 'date-picker__day--sat'), children: d }, d));
|
|
222
235
|
})] })] }), document.body) : null;
|
|
223
236
|
return (_jsxs("div", { className: cx('date-picker', `date-picker--s${size}`, open && 'date-picker--open', className), children: [_jsxs("button", { ref: triggerRef, type: "button", className: "date-picker__trigger body-2", onClick: () => !disabled && toggle(), disabled: disabled, "aria-haspopup": "true", "aria-expanded": open, children: [_jsx("span", { className: value ? 'date-picker__value' : 'date-picker__placeholder', children: value ? formatDisplay(value) : placeholder }), _jsx(Calendar, { size: iconSize, className: "date-picker__icon", strokeWidth: 1.5 })] }), dropdown] }));
|
|
224
237
|
}
|
|
@@ -54,7 +54,10 @@ export default function Dropdown({ options, value, onChange, placeholder, disabl
|
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
/* **열릴 때만** 돈다. options·value 까지 걸어 두면, 열린 동안 부모가 다시 그려지며 options 를 인라인 배열로
|
|
58
|
+
넘길 때마다 화살표로 옮겨 둔 가리킴이 선택값 자리로 되돌아가고 스크롤도 튀었다. */
|
|
59
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
|
+
}, [open]);
|
|
58
61
|
/* eslint-enable react-hooks/set-state-in-effect */
|
|
59
62
|
const toggle = useCallback(() => { if (!disabled)
|
|
60
63
|
rawToggle(); }, [disabled, rawToggle]);
|
|
@@ -102,13 +105,20 @@ export default function Dropdown({ options, value, onChange, placeholder, disabl
|
|
|
102
105
|
select(options[activeIdx]);
|
|
103
106
|
}
|
|
104
107
|
}, [disabled, open, options, activeIdx, select, setOpen]);
|
|
105
|
-
const menu = open ? (_jsx("div", { ref: menuRef, id: listId, className: "dropdown__menu", role: "listbox", style: menuStyle, onMouseLeave: handleMenuLeave,
|
|
108
|
+
const menu = open ? (_jsx("div", { ref: menuRef, id: listId, className: "dropdown__menu", role: "listbox", style: menuStyle, onMouseLeave: handleMenuLeave,
|
|
109
|
+
/* 메뉴 빈 곳·스크롤 막대를 눌러도 트리거에서 포커스가 안 떠나게 — 아래 트리거 onBlur 가 메뉴를 닫지 않도록 */
|
|
110
|
+
onMouseDown: e => e.preventDefault(), children: options.map((opt, i) => (_jsxs("button", { id: `${listId}-option-${i}`, type: "button", role: "option", "aria-selected": opt.value === value, className: cx('dropdown__option body-2', opt.value === value ? 'dropdown__option--active' : '', i === activeIdx ? 'dropdown__option--active' : '', opt.disabled ? 'dropdown__option--disabled' : ''),
|
|
106
111
|
// 선택은 click 에서 한다. onMouseDown 만 두면 마우스로는 눌리지만 스크린리더가
|
|
107
112
|
// 항목을 활성화할 때는 click 만 발생해 **아무 일도 일어나지 않는다**.
|
|
108
113
|
// mousedown 에서는 기본 동작(포커스 이동 → 트리거 blur → 메뉴 닫힘)만 막아,
|
|
109
114
|
// click 이 도달하기 전에 메뉴가 사라지지 않게 한다.
|
|
110
115
|
onMouseDown: e => e.preventDefault(), onClick: () => select(opt), onMouseEnter: () => setActiveIdx(i), disabled: opt.disabled, children: [_jsx("span", { className: "dropdown__option-check", children: opt.value === value && (_jsx("svg", { viewBox: "0 0 14 14", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "2 7 5.5 10.5 12 3.5" }) })) }), opt.label] }, opt.value))) })) : null;
|
|
111
|
-
return (_jsxs("div", { ref: containerRef, className: cx('dropdown', `dropdown--s${size}`, open ? 'dropdown--open' : '', block ? 'dropdown--block' : '', error ? 'dropdown--error' : '', className), onKeyDown: handleKeyDown, children: [_jsxs("button", { ref: triggerRef, type: "button", className: "dropdown__trigger", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listId : undefined, "aria-activedescendant": open && activeIdx >= 0 ? `${listId}-option-${activeIdx}` : undefined, "aria-label": ariaLabel, "aria-invalid": error || undefined, disabled: disabled, onClick: toggle,
|
|
116
|
+
return (_jsxs("div", { ref: containerRef, className: cx('dropdown', `dropdown--s${size}`, open ? 'dropdown--open' : '', block ? 'dropdown--block' : '', error ? 'dropdown--error' : '', className), onKeyDown: handleKeyDown, children: [_jsxs("button", { ref: triggerRef, type: "button", className: "dropdown__trigger", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listId : undefined, "aria-activedescendant": open && activeIdx >= 0 ? `${listId}-option-${activeIdx}` : undefined, "aria-label": ariaLabel, "aria-invalid": error || undefined, disabled: disabled, onClick: toggle,
|
|
117
|
+
/* Tab 으로 포커스가 떠나면 닫는다 — 포커스는 트리거에 두고 aria-activedescendant 로 가리키는 목록이라, 떠난 뒤
|
|
118
|
+
열린 채 남으면 화살표·Enter 가 더는 메뉴로 가지 않는 죽은 메뉴가 된다. 항목·메뉴를 누르는 것은 위에서
|
|
119
|
+
mousedown 기본 동작을 막아 blur 가 나지 않는다. */
|
|
120
|
+
onBlur: e => { if (open && !menuRef.current?.contains(e.relatedTarget))
|
|
121
|
+
setOpen(false); }, children: [_jsx("span", { className: `dropdown__trigger-label body-2${!selected ? ' dropdown__trigger-label--placeholder' : ''}`, children: selected ? selected.label : resolvedPlaceholder }), _jsx("svg", { className: "dropdown__chevron", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "4 6 8 10 12 6" }) })] }), typeof document !== 'undefined' && createPortal(menu,
|
|
112
122
|
// 코어 CSS를 .ihabdevteam-core-scope 로 스코프한 소비자(플레이그라운드 등)에서도
|
|
113
123
|
// body로 portal된 메뉴가 스타일을 잃지 않도록, 가까운 scope 안으로 portal
|
|
114
124
|
// createPortal의 target은 렌더링 시점에 동기적으로 결정되어야 하므로 ref 읽기를 effect로 옮길 수 없다
|
|
@@ -92,16 +92,17 @@ export default function FileUpload({ onFilesSelected, accept, multiple = false,
|
|
|
92
92
|
inputRef.current?.click();
|
|
93
93
|
};
|
|
94
94
|
const handleRemove = (index) => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
/* onRemove 를 주면 알림은 그쪽 한 번으로 끝낸다(onFilesSelected 로 이중으로 알리지 않는다). 다만 비제어 모드에서는
|
|
96
|
+
**보이는 목록도 줄인다** — 전에는 줄이기 전에 돌아가 버려 뺀 파일이 화면에 남았다. 제어 모드(files 를 부모가
|
|
97
|
+
쥠)는 부모가 onRemove 에서 줄인다. */
|
|
98
|
+
onRemove?.(index);
|
|
99
99
|
if (controlledFiles === undefined) {
|
|
100
|
-
/* 뺐다는 것도 알린다. 목록만 줄이고 가만히 있으면 **호출부 손에는 그 파일이
|
|
101
|
-
그대로 남아** 사용자가 뺀 것이 올라간다. */
|
|
102
100
|
const next = files.filter((_, i) => i !== index);
|
|
103
101
|
setInternalFiles(next);
|
|
104
|
-
|
|
102
|
+
/* onRemove 가 없으면 뺐다는 것을 이 길로 알린다. 목록만 줄이고 가만히 있으면 **호출부 손에는 그 파일이
|
|
103
|
+
그대로 남아** 사용자가 뺀 것이 올라간다. */
|
|
104
|
+
if (!onRemove)
|
|
105
|
+
onFilesSelected?.(next);
|
|
105
106
|
}
|
|
106
107
|
};
|
|
107
108
|
return (_jsxs("div", { className: `file-upload ${className}`.trim(), children: [_jsxs("div", { className: cx('file-upload__dropzone', isDragging && 'file-upload__dropzone--dragging', disabled && 'file-upload__dropzone--disabled'), role: "button", tabIndex: disabled ? -1 : 0, "aria-disabled": disabled, onClick: handleBrowseClick, onKeyDown: e => { if (e.key === 'Enter' || e.key === ' ') {
|
|
@@ -25,7 +25,7 @@ export default function FilterPanel({ title, headerAction, collapsible = false,
|
|
|
25
25
|
onCollapsedChange?.(next);
|
|
26
26
|
};
|
|
27
27
|
const showHeader = title || headerAction || collapsible;
|
|
28
|
-
return (_jsxs("div", { className: cx('filter-panel', collapsible ? 'filter-panel--collapsible' : '', collapsed ? 'filter-panel--collapsed' : '', className), children: [showHeader && (_jsxs("div", { className: "filter-panel__header", children: [_jsxs("div", { className: "filter-panel__title-row", children: [collapsible && (_jsx(Button, { size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-down' : 'chevron-up',
|
|
28
|
+
return (_jsxs("div", { className: cx('filter-panel', collapsible ? 'filter-panel--collapsible' : '', collapsed ? 'filter-panel--collapsed' : '', className), children: [showHeader && (_jsxs("div", { className: "filter-panel__header", children: [_jsxs("div", { className: "filter-panel__title-row", children: [collapsible && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-down' : 'chevron-up',
|
|
29
29
|
/* Button은 aria-label을 자기 ariaLabel prop으로 덮어쓴다({...rest} 뒤에
|
|
30
30
|
aria-label={ariaLabelString}을 다시 쓴다) — kebab으로 넘기면 undefined가
|
|
31
31
|
되어 이름이 통째로 사라진다. 저장소의 다른 호출부는 모두 ariaLabel을 쓴다.
|
|
@@ -55,7 +55,7 @@ export default function FilterPanel({ title, headerAction, collapsible = false,
|
|
|
55
55
|
const descriptionTooltip = typeof item !== 'string' ? item.descriptionTooltip : undefined;
|
|
56
56
|
const shape = typeof item !== 'string' ? item.shape : undefined;
|
|
57
57
|
const isSelected = Array.isArray(sel) ? sel.includes(value) : sel === value;
|
|
58
|
-
return (_jsx(Button, { size: "keyboard", label: itemLabels ?? displayLabel ?? value, type: section.itemType ?? (itemLabels ? 'bar' : value.length === 1 ? 'dot' : undefined), selected: isSelected, disabled: disabled, shape: shape, showTooltipInfo: !!tooltip, tooltipInfoText: tooltip, tooltipDescription: descriptionTooltip, onClick: () => onSelect?.(value, section.label) }, value));
|
|
58
|
+
return (_jsx(Button, { htmlType: "button", size: "keyboard", label: itemLabels ?? displayLabel ?? value, type: section.itemType ?? (itemLabels ? 'bar' : value.length === 1 ? 'dot' : undefined), selected: isSelected, disabled: disabled, shape: shape, showTooltipInfo: !!tooltip, tooltipInfoText: tooltip, tooltipDescription: descriptionTooltip, onClick: () => onSelect?.(value, section.label) }, value));
|
|
59
59
|
};
|
|
60
60
|
return (_jsxs(Fragment, { children: [section.groupLabel && (_jsxs("div", { className: "filter-panel__group-label-row", children: [_jsx("div", { className: "filter-panel__group-label body-2", children: section.groupLabel }), section.groupAction] })), _jsxs("div", { className: cx('filter-panel__section', section.layout === 'row' ? 'filter-panel__section--row' : '', section.block ? 'filter-panel__section--block' : ''), children: [_jsxs("div", { className: "filter-panel__section-label-row", children: [_jsx("span", { className: "filter-panel__section-label body-3", children: section.displayLabel ?? section.label }), section.labelAction ?? (section.labelInfo ? _jsx(FilterSectionInfo, { ...section.labelInfo }) : null)] }), section.type === 'note' ? (_jsx("div", { className: "filter-panel__note body-3", children: section.content })) : section.type === 'input' ? (_jsx(TextField, { className: "filter-panel__textfield", placeholder: section.placeholder, ariaLabel: section.displayLabel ?? section.label, value: selected[section.label] ?? '', onChange: (value) => onSelect?.(value, section.label), error: section.error })) : (!section.multiple && section.display === 'segmented') ? (_jsx(SegmentedControl
|
|
61
61
|
/* 바로 위 TextField 처럼 섹션 이름을 그대로 쓴다. 안 주면 라디오그룹이
|
|
@@ -73,7 +73,10 @@ export default function FilterPanel({ title, headerAction, collapsible = false,
|
|
|
73
73
|
const disabled = typeof item !== 'string' ? item.disabled : undefined;
|
|
74
74
|
return { key: value, label: displayLabel ?? value, disabled };
|
|
75
75
|
}),
|
|
76
|
-
],
|
|
76
|
+
],
|
|
77
|
+
/* 아무것도 안 골랐으면 빈 값 — undefined 를 넘기면 SegmentedControl 의 기본값(첫 보기)이 걸려, 상태는
|
|
78
|
+
비었는데 첫 보기가 골라진 것처럼 보였다(필터는 걸리지 않았는데 걸린 듯). */
|
|
79
|
+
value: isAllSelected && section.allOption ? FILTER_ALL_VALUE : selected[section.label] ?? '', onChange: (value) => onSelect?.(value, section.label), size: 32 })) : (!section.multiple && section.display === 'dropdown') ? (_jsx(Dropdown, { className: "filter-panel__dropdown", size: 40, options: [
|
|
77
80
|
...(section.allOption ? [{ value: FILTER_ALL_VALUE, label: section.allLabel ?? t('common.all', '전체') }] : []),
|
|
78
81
|
...items.map(item => {
|
|
79
82
|
const value = typeof item === 'string' ? item : item.value;
|
|
@@ -81,8 +84,8 @@ export default function FilterPanel({ title, headerAction, collapsible = false,
|
|
|
81
84
|
const disabled = typeof item !== 'string' ? item.disabled : undefined;
|
|
82
85
|
return { value, label: displayLabel ?? value, disabled };
|
|
83
86
|
}),
|
|
84
|
-
], value: isAllSelected && section.allOption ? FILTER_ALL_VALUE : selected[section.label], onChange: (value) => onSelect?.(value, section.label) })) : (_jsxs("div", { className: section.groups ? 'filter-panel__item-groups' : 'filter-panel__items', children: [section.allOption && !section.groups && (_jsx(Button, { size: "keyboard", label: section.allLabel ?? t('common.all', '전체'), selected: isAllSelected, disabled: section.allOptionDisabled, onClick: () => onSelect?.(FILTER_ALL_VALUE, section.label) })), section.groups
|
|
85
|
-
? section.groups.map((group, gi) => (_jsxs("div", { className: "filter-panel__item-group", children: [gi === 0 && section.allOption && (_jsx(Button, { size: "keyboard", label: section.allLabel ?? t('common.all', '전체'), selected: isAllSelected, disabled: section.allOptionDisabled, onClick: () => onSelect?.(FILTER_ALL_VALUE, section.label) })), group
|
|
87
|
+
], value: isAllSelected && section.allOption ? FILTER_ALL_VALUE : selected[section.label], onChange: (value) => onSelect?.(value, section.label) })) : (_jsxs("div", { className: section.groups ? 'filter-panel__item-groups' : 'filter-panel__items', children: [section.allOption && !section.groups && (_jsx(Button, { htmlType: "button", size: "keyboard", label: section.allLabel ?? t('common.all', '전체'), selected: isAllSelected, disabled: section.allOptionDisabled, onClick: () => onSelect?.(FILTER_ALL_VALUE, section.label) })), section.groups
|
|
88
|
+
? section.groups.map((group, gi) => (_jsxs("div", { className: "filter-panel__item-group", children: [gi === 0 && section.allOption && (_jsx(Button, { htmlType: "button", size: "keyboard", label: section.allLabel ?? t('common.all', '전체'), selected: isAllSelected, disabled: section.allOptionDisabled, onClick: () => onSelect?.(FILTER_ALL_VALUE, section.label) })), group
|
|
86
89
|
.filter((item) => item != null)
|
|
87
90
|
.map(renderItemButton)] }, gi)))
|
|
88
91
|
: items.map(renderItemButton)] }))] })] }, section.label));
|
|
@@ -11,5 +11,5 @@ import { useHoverAnchor } from '../hooks/useHoverAnchor';
|
|
|
11
11
|
export default function FilterSectionInfo({ title, items }) {
|
|
12
12
|
const { t } = useTranslation();
|
|
13
13
|
const { anchorRect, openAt, close: handleClose } = useHoverAnchor();
|
|
14
|
-
return (_jsxs(_Fragment, { children: [_jsx(Button, { size: 24, type: "dot", icon: "info", variant: "transparent-gray", useBorder: false, selected: !!anchorRect, ariaLabel: t('common.info', '설명'), onPointerEnter: (e) => openAt(e.currentTarget), onPointerLeave: handleClose, onFocus: (e) => openAt(e.currentTarget), onBlur: handleClose }), anchorRect && (_jsx(DescriptionTooltip, { title: title, items: items, showIndex: items.length > 1, anchorRect: anchorRect, onClose: handleClose }))] }));
|
|
14
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { htmlType: "button", size: 24, type: "dot", icon: "info", variant: "transparent-gray", useBorder: false, selected: !!anchorRect, ariaLabel: t('common.info', '설명'), onPointerEnter: (e) => openAt(e.currentTarget), onPointerLeave: handleClose, onFocus: (e) => openAt(e.currentTarget), onBlur: handleClose }), anchorRect && (_jsx(DescriptionTooltip, { title: title, items: items, showIndex: items.length > 1, anchorRect: anchorRect, onClose: handleClose }))] }));
|
|
15
15
|
}
|
|
@@ -112,7 +112,7 @@ export default function Header({ activeKey, onChange, items, malgwiItems: _malgw
|
|
|
112
112
|
onClick: () => { it.onClick?.(); closeUtilMenu(); },
|
|
113
113
|
})),
|
|
114
114
|
}], [bottomMoreItems, activeKey, closeUtilMenu]);
|
|
115
|
-
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, { size: 48, type: "dot", shape: "pill", variant: "default", icon: 'menu', ariaLabel: t('header.menuOpen', '메뉴 열기'), onClick: toggleUtilMenu, "aria-expanded": utilOpen, "aria-controls": utilOpen ? utilMenuId : undefined }), 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, { items: items, activeKey: activeKey ?? undefined, onChange: onChange, unreadCount: unreadCount, onDebug: onDebug, onNotification: onNotification, onAccount: onAccount, onSetting: onSetting, onHelp: onHelp, appMode: appMode, onAppModeChange: onAppModeChange }), isMobile && _jsx(BottomNav, { items: bottomItems, activeKey: activeKey ?? undefined, onChange: handleBottomChange, appMode: appMode, onAppModeChange: onAppModeChange })] }));
|
|
115
|
+
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", icon: 'menu', ariaLabel: t('header.menuOpen', '메뉴 열기'), onClick: toggleUtilMenu, "aria-expanded": utilOpen, "aria-controls": utilOpen ? utilMenuId : undefined }), 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, { items: items, activeKey: activeKey ?? undefined, onChange: onChange, unreadCount: unreadCount, onDebug: onDebug, onNotification: onNotification, onAccount: onAccount, onSetting: onSetting, onHelp: onHelp, appMode: appMode, onAppModeChange: onAppModeChange }), isMobile && _jsx(BottomNav, { items: bottomItems, activeKey: activeKey ?? undefined, onChange: handleBottomChange, appMode: appMode, onAppModeChange: onAppModeChange })] }));
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
118
|
* `Header`의 alias입니다. 프로젝트 내 `Header`라는 이름이 이미 사용 중일 때 충돌 없이 임포트할 수 있습니다.
|
|
@@ -8,6 +8,6 @@ export default function HearingDeviceInfo({ devices, onAddDevice, onOpenList, on
|
|
|
8
8
|
return (_jsxs("div", { className: "hearing-device-info", children: [_jsx("div", { className: "hearing-device-info__title body-3", children: t('customers.device.title', '청각재활기기') }), _jsx("div", { className: "hearing-device-info__row", children: ['좌', '우'].map(side => {
|
|
9
9
|
const d = devices[side];
|
|
10
10
|
const sideLabel = side === '좌' ? t('device.direction.left', '좌') : t('device.direction.right', '우');
|
|
11
|
-
return (_jsxs("div", { className: "hearing-device-info__side", children: [_jsx("span", { className: "hearing-device-info__side-label body-3", children: sideLabel }), d ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "hearing-device-info__device", children: [_jsx("span", { className: "hearing-device-info__device-name body-3", children: d.brand }), _jsxs("span", { className: "hearing-device-info__device-detail", children: [d.device, d.fitType ? ` · ${d.fitType}` : ''] }), d.productName && _jsx("span", { className: "hearing-device-info__device-product", children: d.productName }), _jsxs("span", { className: "hearing-device-info__device-date", children: [d.wornYear, ".", d.wornMonth, ".", d.wornDay] })] }), _jsxs("div", { className: "hearing-device-info__actions", children: [_jsx(Button, { size: 40, type: "dot", icon: "plus", onClick: () => onAddDevice(side), showTooltipInfo: t('device.addNew', '새 기기 추가'), ariaLabel: t('device.addNew', '새 기기 추가') }), _jsx(Button, { size: 40, type: "dot", icon: "list", onClick: () => onOpenList(side), showTooltipInfo: t('device.selectFromList', '목록에서 선택'), ariaLabel: t('device.selectFromList', '목록에서 선택') }), _jsx(Button, { size: 40, type: "dot", icon: "x", variant: "danger", onClick: () => onRemoveDevice(side), showTooltipInfo: t('device.removeDevice', '착용 해제'), ariaLabel: t('device.removeDevice', '착용 해제') })] })] })) : (_jsxs("div", { className: "hearing-device-info__actions", children: [_jsx(Button, { size: 40, type: "dot", icon: "plus", onClick: () => onAddDevice(side), showTooltipInfo: t('device.addNew', '새 기기 추가'), ariaLabel: t('device.addNew', '새 기기 추가') }), _jsx(Button, { size: 40, type: "dot", icon: "list", onClick: () => onOpenList(side), showTooltipInfo: t('device.selectFromList', '목록에서 선택'), ariaLabel: t('device.selectFromList', '목록에서 선택') })] }))] }, side));
|
|
11
|
+
return (_jsxs("div", { className: "hearing-device-info__side", children: [_jsx("span", { className: "hearing-device-info__side-label body-3", children: sideLabel }), d ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "hearing-device-info__device", children: [_jsx("span", { className: "hearing-device-info__device-name body-3", children: d.brand }), _jsxs("span", { className: "hearing-device-info__device-detail", children: [d.device, d.fitType ? ` · ${d.fitType}` : ''] }), d.productName && _jsx("span", { className: "hearing-device-info__device-product", children: d.productName }), _jsxs("span", { className: "hearing-device-info__device-date", children: [d.wornYear, ".", d.wornMonth, ".", d.wornDay] })] }), _jsxs("div", { className: "hearing-device-info__actions", children: [_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "plus", onClick: () => onAddDevice(side), showTooltipInfo: t('device.addNew', '새 기기 추가'), ariaLabel: t('device.addNew', '새 기기 추가') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "list", onClick: () => onOpenList(side), showTooltipInfo: t('device.selectFromList', '목록에서 선택'), ariaLabel: t('device.selectFromList', '목록에서 선택') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "danger", onClick: () => onRemoveDevice(side), showTooltipInfo: t('device.removeDevice', '착용 해제'), ariaLabel: t('device.removeDevice', '착용 해제') })] })] })) : (_jsxs("div", { className: "hearing-device-info__actions", children: [_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "plus", onClick: () => onAddDevice(side), showTooltipInfo: t('device.addNew', '새 기기 추가'), ariaLabel: t('device.addNew', '새 기기 추가') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "list", onClick: () => onOpenList(side), showTooltipInfo: t('device.selectFromList', '목록에서 선택'), ariaLabel: t('device.selectFromList', '목록에서 선택') })] }))] }, side));
|
|
12
12
|
}) })] }));
|
|
13
13
|
}
|
|
@@ -13,7 +13,7 @@ export default function PageTitle({ title, sub, rightAction, titleAction, badge,
|
|
|
13
13
|
return (_jsxs("div", { className: wrap ? 'page-title page-title--wrap' : 'page-title', children: [_jsxs("div", { className: "page-title__main", children: [_jsxs("div", { className: "page-title__title-row", children: [onBack && (
|
|
14
14
|
/* 글자 없이 화살표만 둔다 — 제목 옆에서 '뒤로' 라고 적으면 제목과 경쟁한다.
|
|
15
15
|
그만큼 과녁을 크게(48) 잡는다. 이름은 낭독기용으로 따로 준다. */
|
|
16
|
-
_jsx(Button, { className: "page-title__back", size: 48, type: "dot", icon: "chevron-left", iconSize: "large", useBorder: false,
|
|
16
|
+
_jsx(Button, { htmlType: "button", className: "page-title__back", size: 48, type: "dot", icon: "chevron-left", iconSize: "large", useBorder: false,
|
|
17
17
|
/* 역할 이름을 쓴다 — `--color-black` 은 다크에서 이름과 실제 색이 뒤집힌다. */
|
|
18
18
|
iconColor: "var(--color-text-primary)", ariaLabel: 뒤로이름, showTooltipInfo: 뒤로이름, onClick: onBack })), _jsx("h1", { className: "page-title__title h1", children: title }), badge && _jsx("div", { className: "page-title__badge", children: _jsx(Badge, { label: badge, size: "small", variant: badgeVariant, fill: "outline" }) }), titleAction ? _jsx("div", { className: "page-title__title-action", children: titleAction }) : null] }), sub && _jsx("p", { className: "page-title__sub body-2", children: sub })] }), rightAction ? _jsx("div", { className: "page-title__action", children: rightAction }) : null] }));
|
|
19
19
|
}
|
|
@@ -16,5 +16,5 @@ export default function Pagination({ page, totalPages, onChange, prevLabel, next
|
|
|
16
16
|
const resolvedNext = nextLabel ?? t('common.nextPage', '다음 페이지');
|
|
17
17
|
const handlePrev = () => onChange(Math.max(1, page - 1));
|
|
18
18
|
const handleNext = () => onChange(Math.min(totalPages, page + 1));
|
|
19
|
-
return (_jsxs("div", { className: cx('pagination', className), children: [_jsx(Button, { size: 32, type: "dot", shape: "rounded", icon: "chevron-left", ariaLabel: resolvedPrev, disabled: page <= 1, onClick: handlePrev }), _jsxs("span", { className: "pagination__info body-3", children: [page, " / ", totalPages] }), _jsx(Button, { size: 32, type: "dot", shape: "rounded", icon: "chevron-right", ariaLabel: resolvedNext, disabled: page >= totalPages, onClick: handleNext })] }));
|
|
19
|
+
return (_jsxs("div", { className: cx('pagination', className), children: [_jsx(Button, { htmlType: "button", size: 32, type: "dot", shape: "rounded", icon: "chevron-left", ariaLabel: resolvedPrev, disabled: page <= 1, onClick: handlePrev }), _jsxs("span", { className: "pagination__info body-3", children: [page, " / ", totalPages] }), _jsx(Button, { htmlType: "button", size: 32, type: "dot", shape: "rounded", icon: "chevron-right", ariaLabel: resolvedNext, disabled: page >= totalPages, onClick: handleNext })] }));
|
|
20
20
|
}
|
|
@@ -89,10 +89,10 @@ export function PanelRow(props) {
|
|
|
89
89
|
else if (variant === 'button') {
|
|
90
90
|
const p = props;
|
|
91
91
|
if (p.buttons?.length) {
|
|
92
|
-
right = (_jsx("div", { className: "panel-row__button-group", children: p.buttons.map((btn, i) => (_jsx(Button, { size: btn.btnSize ?? 40, variant: btn.btnVariant ?? 'default', disabled: btn.disabled, onClick: btn.onClick, children: btn.label }, i))) }));
|
|
92
|
+
right = (_jsx("div", { className: "panel-row__button-group", children: p.buttons.map((btn, i) => (_jsx(Button, { htmlType: "button", size: btn.btnSize ?? 40, variant: btn.btnVariant ?? 'default', disabled: btn.disabled, onClick: btn.onClick, children: btn.label }, i))) }));
|
|
93
93
|
}
|
|
94
94
|
else {
|
|
95
|
-
right = (_jsx(Button, { size: p.btnSize ?? 40, variant: p.btnVariant ?? 'default', disabled: p.disabled, onClick: p.onClick, children: p.buttonLabel }));
|
|
95
|
+
right = (_jsx(Button, { htmlType: "button", size: p.btnSize ?? 40, variant: p.btnVariant ?? 'default', disabled: p.disabled, onClick: p.onClick, children: p.buttonLabel }));
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
else if (variant === 'text') {
|
|
@@ -143,6 +143,6 @@ export function PanelRowDevice({ devices, onRelease, emptyLabel }) {
|
|
|
143
143
|
/* 표의 줄 끝 단추와 같은 모양·같은 말 — 하는 일도 같다.
|
|
144
144
|
이력에서 지우는 것이 아니라 '지금 끼고 있음' 만 내린다
|
|
145
145
|
(잠시 안 씀·수리 보냄 같은 경우가 실제로 있다). */
|
|
146
|
-
_jsx(Button, { size: 32, type: "dot", icon: "x", ariaLabel: t('customers.device.unwear', '착용 해제'), showTooltipInfo: t('customers.device.unwear', '착용 해제'), onClick: () => onRelease(side) }))] }), d ? (_jsxs("div", { className: "panel-row__device-info", children: [_jsxs("span", { className: "panel-row__device-kind", children: [d.device && _jsx(Badge, { size: "small", variant: deviceKindVariant(d.device), fill: "soft", label: d.device }), 메타(d) && _jsx("span", { className: "panel-row__device-detail body-3", children: 메타(d) })] }), _jsx("span", { className: "panel-row__device-name body-2b", children: d.productName || d.brand }), _jsx("span", { className: "panel-row__device-date caption-1", children: t('customers.device.wornSince', { date: `${d.wornYear}.${d.wornMonth}.${d.wornDay}` }) })] })) : (_jsx("span", { className: "panel-row__device-empty body-3", children: emptyLabel ?? t('customers.device.notRegistered', '미등록') }))] }, side));
|
|
146
|
+
_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "x", ariaLabel: t('customers.device.unwear', '착용 해제'), showTooltipInfo: t('customers.device.unwear', '착용 해제'), onClick: () => onRelease(side) }))] }), d ? (_jsxs("div", { className: "panel-row__device-info", children: [_jsxs("span", { className: "panel-row__device-kind", children: [d.device && _jsx(Badge, { size: "small", variant: deviceKindVariant(d.device), fill: "soft", label: d.device }), 메타(d) && _jsx("span", { className: "panel-row__device-detail body-3", children: 메타(d) })] }), _jsx("span", { className: "panel-row__device-name body-2b", children: d.productName || d.brand }), _jsx("span", { className: "panel-row__device-date caption-1", children: t('customers.device.wornSince', { date: `${d.wornYear}.${d.wornMonth}.${d.wornDay}` }) })] })) : (_jsx("span", { className: "panel-row__device-empty body-3", children: emptyLabel ?? t('customers.device.notRegistered', '미등록') }))] }, side));
|
|
147
147
|
}) }));
|
|
148
148
|
}
|
|
@@ -79,7 +79,7 @@ export default function PaymentPlan(props) {
|
|
|
79
79
|
null) : isInquiry ? (
|
|
80
80
|
/* 문의는 실제로 할 일이 있는 행동이라 버튼을 둔다 — 페이지 하단 문의 버튼과
|
|
81
81
|
같은 공용 Button(variant=main)으로 맞춘다. */
|
|
82
|
-
_jsx(Button, { className: "payment-plan__contact-button", label: t('payment.plan.contact', { defaultValue: '문의하기' }), variant: "main", size: 56, onClick: () => plan.tiers?.[0] && onSelect?.({ ...plan.tiers[0], productId: plan.id, productTitle: plan.title }) })) : (_jsx(_Fragment, { children: _jsx("div", { className: "payment-plan__pricing-list", children: (plan.tiers || []).map((tier) => {
|
|
82
|
+
_jsx(Button, { htmlType: "button", className: "payment-plan__contact-button", label: t('payment.plan.contact', { defaultValue: '문의하기' }), variant: "main", size: 56, onClick: () => plan.tiers?.[0] && onSelect?.({ ...plan.tiers[0], productId: plan.id, productTitle: plan.title }) })) : (_jsx(_Fragment, { children: _jsx("div", { className: "payment-plan__pricing-list", children: (plan.tiers || []).map((tier) => {
|
|
83
83
|
/* 라벨이 없을 때의 대체말도 **화면에 뜨는 글**이다 — 사전에서 가져온다.
|
|
84
84
|
아래 정규식의 '오픈 특가' 는 다르다. 그건 앱이 넘겨준 **데이터를
|
|
85
85
|
읽는** 것이라 한국어 그대로여야 한다. */
|
|
@@ -40,7 +40,7 @@ export default function DeviceSideTable({ rows, onSelect, onDelete, onWear, onRe
|
|
|
40
40
|
}, [rows, 정렬]);
|
|
41
41
|
/* 개수 보기·쪽넘김은 방문 표와 같은 것을 쓴다 — 같은 자리에서 같은 뜻으로 읽히게 */
|
|
42
42
|
const { pageRows, pageSizeProps, paginationProps } = useTablePaging(sortedRows, 정렬);
|
|
43
|
-
return (_jsxs("div", { className: "dlp-side-table", children: [_jsx(Toolbar, { count: { value: rows.length, unit: t('customers.device.unit', '개') }, pageSize: pageSizeProps, pagination: paginationProps, actions: onAdd && (_jsx(Button, { size: 40, type: "bar", variant: "main", icon: "plus", label: t('customers.device.addButton', '기기 추가'), onClick: onAdd })) }), _jsx(Table, { resizable: true, resizeMode: "fit", resizeStorageKey: "customer-device-history-v2", fields: [
|
|
43
|
+
return (_jsxs("div", { className: "dlp-side-table", children: [_jsx(Toolbar, { count: { value: rows.length, unit: t('customers.device.unit', '개') }, pageSize: pageSizeProps, pagination: paginationProps, actions: onAdd && (_jsx(Button, { htmlType: "button", size: 40, type: "bar", variant: "main", icon: "plus", label: t('customers.device.addButton', '기기 추가'), onClick: onAdd })) }), _jsx(Table, { resizable: true, resizeMode: "fit", resizeStorageKey: "customer-device-history-v2", fields: [
|
|
44
44
|
/* 맨 왼쪽 — 지금 끼고 있는 것만 좌/우를 적는다. 이 열만 훑으면 "오늘 이 사람이
|
|
45
45
|
무엇을 쓰고 있나" 가 한눈에 보인다(나머지 줄은 비어 있어 눈에 안 걸린다). */
|
|
46
46
|
/* 체크박스 칸은 폭을 늘릴 것이 없다 — 손잡이를 주면 잡히는데 아무 일도
|
|
@@ -83,7 +83,7 @@ export default function DeviceSideTable({ rows, onSelect, onDelete, onWear, onRe
|
|
|
83
83
|
못 하는 일을 내밀지 않는다. 눌림은 줄로 새지 않게 막는다(줄을 누르면
|
|
84
84
|
수정 창이 열린다). */
|
|
85
85
|
...(onDelete ? [{
|
|
86
|
-
content: (_jsx("span", { className: "dlp-row-actions", children: _jsx(Button, { size: 28, type: "bar", variant: "transparent-gray", label: t('common.delete', '삭제'), onClick: e => { e?.stopPropagation(); onDelete(row); } }) })),
|
|
86
|
+
content: (_jsx("span", { className: "dlp-row-actions", children: _jsx(Button, { htmlType: "button", size: 28, type: "bar", variant: "transparent-gray", label: t('common.delete', '삭제'), onClick: e => { e?.stopPropagation(); onDelete(row); } }) })),
|
|
87
87
|
}] : []),
|
|
88
88
|
], onRowClick: ({ row }) => onSelect(row),
|
|
89
89
|
/* Table 이 이미 TableRow variant="empty" 로 감싼다 — div 를 한 겹 더 씌우면
|
|
@@ -38,7 +38,7 @@ const DeleteButton = ({ onClick, ariaLabel }) => {
|
|
|
38
38
|
/* 화면낭독기가 읽는 이름이라 사전에서 온다 — 여기 박아 두면 영어로 쓰는 사람에게
|
|
39
39
|
이 버튼만 '삭제' 로 읽힌다. */
|
|
40
40
|
const { t } = useTranslation();
|
|
41
|
-
return (_jsx(Button, { size: 32, type: "dot", icon: "x", variant: "transparent-lightGray", className: "load-table-popup__delete", onClick: onClick, ariaLabel: ariaLabel ?? t('common.delete', '삭제'), showTooltipInfo: true }));
|
|
41
|
+
return (_jsx(Button, { htmlType: "button", size: 32, type: "dot", icon: "x", variant: "transparent-lightGray", className: "load-table-popup__delete", onClick: onClick, ariaLabel: ariaLabel ?? t('common.delete', '삭제'), showTooltipInfo: true }));
|
|
42
42
|
};
|
|
43
43
|
export default function LoadTablePopup({ onLoad, onCancel, tables: tablesProp, onDelete: onDeleteProp }) {
|
|
44
44
|
const { t } = useTranslation();
|
|
@@ -81,7 +81,7 @@ export default function LoadTablePopup({ onLoad, onCancel, tables: tablesProp, o
|
|
|
81
81
|
animateThen(() => onLoad(table));
|
|
82
82
|
}, [tables, selected, onLoad]);
|
|
83
83
|
const headerActions = useMemo(() => tables.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Dropdown, { options: VIEW_OPTIONS, value: viewMode, onChange: handleSetViewMode }), _jsx(Dropdown, { options: SORT_OPTIONS, value: sortKey, onChange: handleSetSortKey })] })) : undefined, [tables.length, viewMode, sortKey, handleSetViewMode, handleSetSortKey, VIEW_OPTIONS, SORT_OPTIONS]);
|
|
84
|
-
return (_jsx(Popup, { className: "popup--load-table", title: t('wordTable.loadTable.title', '저장된 단어 표 불러오기'), headerActions: headerActions, onClose: onCancel, footer: ({ animateThen, closeWithAnimation }) => (_jsxs("div", { className: "load-table-popup__footer-actions", children: [_jsx(Button, { size: 48, type: "bar", label: t('common.cancel', '취소'), onClick: closeWithAnimation, width: "fit-content" }), _jsx(Button, { size: 48, type: "bar", variant: "main", label: t('wordTable.loadTable.loadButton', '불러오기'), width: "fit-content", disabled: !selected, onClick: () => handleLoadSelected(animateThen) })] })), children: _jsx("div", { className: "load-table-popup__body", children: tables.length === 0 ? (_jsx("p", { className: "load-table-popup__empty body-2", children: t('wordTable.loadTable.empty', '저장된 단어 표가 없습니다.') })) : (_jsxs(_Fragment, { children: [viewMode === 'grid' && (_jsx("ul", { className: "load-table-popup__grid", children: sorted.map(table => (_jsxs("li", { className: `load-table-popup__card${selected === table.id ? ' load-table-popup__card--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), children: [_jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', '삭제') }), _jsx(TableThumbnail, { table: table }), _jsxs("div", { className: "load-table-popup__card-info", children: [_jsx("span", { className: "load-table-popup__card-name body-2", children: table.name }), _jsxs("span", { className: "load-table-popup__card-meta caption-1", children: [formatDate(table.savedAt), " \u00B7 ", table.items.length, "\uAC1C"] })] })] }, table.id))) })), viewMode === 'list' && (_jsx("ul", { className: "load-table-popup__list", children: sorted.map(table => (_jsxs("li", { className: `load-table-popup__row${selected === table.id ? ' load-table-popup__row--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), children: [_jsx("span", { className: "load-table-popup__row-check", children: selected === table.id && (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: _jsx("polyline", { points: "4 13 9 18 20 6", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) })) }), _jsxs("div", { className: "load-table-popup__row-main", children: [_jsx("span", { className: "load-table-popup__row-name body-2", children: table.name }), _jsx("div", { className: "load-table-popup__row-words", children: table.items.map((item, i) => (_jsx("span", { className: "load-table-popup__row-word caption-1", children: item.words.length >= 2
|
|
84
|
+
return (_jsx(Popup, { className: "popup--load-table", title: t('wordTable.loadTable.title', '저장된 단어 표 불러오기'), headerActions: headerActions, onClose: onCancel, footer: ({ animateThen, closeWithAnimation }) => (_jsxs("div", { className: "load-table-popup__footer-actions", children: [_jsx(Button, { htmlType: "button", size: 48, type: "bar", label: t('common.cancel', '취소'), onClick: closeWithAnimation, width: "fit-content" }), _jsx(Button, { htmlType: "button", size: 48, type: "bar", variant: "main", label: t('wordTable.loadTable.loadButton', '불러오기'), width: "fit-content", disabled: !selected, onClick: () => handleLoadSelected(animateThen) })] })), children: _jsx("div", { className: "load-table-popup__body", children: tables.length === 0 ? (_jsx("p", { className: "load-table-popup__empty body-2", children: t('wordTable.loadTable.empty', '저장된 단어 표가 없습니다.') })) : (_jsxs(_Fragment, { children: [viewMode === 'grid' && (_jsx("ul", { className: "load-table-popup__grid", children: sorted.map(table => (_jsxs("li", { className: `load-table-popup__card${selected === table.id ? ' load-table-popup__card--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), children: [_jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', '삭제') }), _jsx(TableThumbnail, { table: table }), _jsxs("div", { className: "load-table-popup__card-info", children: [_jsx("span", { className: "load-table-popup__card-name body-2", children: table.name }), _jsxs("span", { className: "load-table-popup__card-meta caption-1", children: [formatDate(table.savedAt), " \u00B7 ", table.items.length, "\uAC1C"] })] })] }, table.id))) })), viewMode === 'list' && (_jsx("ul", { className: "load-table-popup__list", children: sorted.map(table => (_jsxs("li", { className: `load-table-popup__row${selected === table.id ? ' load-table-popup__row--selected' : ''}`, onClick: () => setSelected(table.id), ...pressable(() => setSelected(table.id)), children: [_jsx("span", { className: "load-table-popup__row-check", children: selected === table.id && (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: _jsx("polyline", { points: "4 13 9 18 20 6", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) })) }), _jsxs("div", { className: "load-table-popup__row-main", children: [_jsx("span", { className: "load-table-popup__row-name body-2", children: table.name }), _jsx("div", { className: "load-table-popup__row-words", children: table.items.map((item, i) => (_jsx("span", { className: "load-table-popup__row-word caption-1", children: item.words.length >= 2
|
|
85
85
|
? `${item.words[0].label} · ${item.words[1].label}`
|
|
86
86
|
: item.words[0]?.label ?? item.id }, i))) })] }), _jsxs("span", { className: "load-table-popup__row-count caption-1", children: [table.items.length, "\uAC1C"] }), _jsx("span", { className: "load-table-popup__row-date caption-1", children: formatDate(table.savedAt) }), _jsx(DeleteButton, { onClick: e => handleDelete(table.id, e), ariaLabel: t('wordTable.loadTable.deleteAriaLabel', '삭제') })] }, table.id))) }))] })) }) }));
|
|
87
87
|
}
|
|
@@ -93,7 +93,14 @@ export default function Popup(props) {
|
|
|
93
93
|
const first = panel.querySelector(FOCUSABLE_SELECTOR);
|
|
94
94
|
(first ?? panel).focus();
|
|
95
95
|
}
|
|
96
|
-
return () => {
|
|
96
|
+
return () => {
|
|
97
|
+
/* 포커스가 **아직 이 창 안이거나 갈 곳을 잃었을 때만** 되돌린다. 닫으면서 다음 창을 여는 흐름
|
|
98
|
+
(미리보기 → 삭제 확인)에서는 닫힘 애니메이션(260ms) 뒤에 여기가 돌 때 포커스가 이미 새 창에 있는데,
|
|
99
|
+
전에는 그것을 뒤 화면의 여는 단추로 빼앗아 Tab 가둠이 풀리고 Enter 가 뒤 화면을 눌렀다. */
|
|
100
|
+
const active = document.activeElement;
|
|
101
|
+
if (!active || active === document.body || (panel && panel.contains(active)))
|
|
102
|
+
previouslyFocused?.focus?.();
|
|
103
|
+
};
|
|
97
104
|
}, [inline]);
|
|
98
105
|
/* Tab 순환을 패널 안으로 가둔다 — 네 곳에 있던 같은 열두 줄을 훅 한 벌로 모았다.
|
|
99
106
|
inline 으로 쓸 때는 다이얼로그가 아니라 걸지 않는다(예전과 같다). */
|
|
@@ -147,9 +154,14 @@ export default function Popup(props) {
|
|
|
147
154
|
if (pressed && e.target === e.currentTarget)
|
|
148
155
|
closeWithAnimation();
|
|
149
156
|
};
|
|
150
|
-
const el = (_jsx("div", { className: `popup ${inline ? 'popup--inline' : ''} ${closing ? 'popup--closing' : ''} ${className}`, onPointerDown: backdropEnabled ? onBackdropPointerDown : undefined, onClick: backdropEnabled ? onBackdropClick : undefined,
|
|
157
|
+
const el = (_jsx("div", { className: `popup ${inline ? 'popup--inline' : ''} ${closing ? 'popup--closing' : ''} ${className}`, onPointerDown: backdropEnabled ? onBackdropPointerDown : undefined, onClick: backdropEnabled ? onBackdropClick : undefined,
|
|
158
|
+
/* 바탕을 눌러도 포커스는 판 안에 둔다 — 닫히지 않는 바탕(closeOnBackdrop={false}·closeDisabled·beforeClose 가
|
|
159
|
+
거절)을 누르면 포커스가 body 로 떨어져, 이어서 누른 Tab 이 뒤 화면으로 나갔다(가둠은 판 안의 키만 듣는다).
|
|
160
|
+
mousedown 의 기본 동작(포커스 옮김)만 막고 click 은 그대로 온다. */
|
|
161
|
+
onMouseDown: inline ? undefined : (e) => { if (e.target === e.currentTarget)
|
|
162
|
+
e.preventDefault(); }, children: _jsxs("div", { ref: panelRef, className: `popup-panel${size ? ` popup-panel--${size}` : ''}`,
|
|
151
163
|
/* max-height(CSS)가 height 보다 이기므로 화면보다 크게 줘도 화면에 맞춰진다 */
|
|
152
|
-
style: height != null ? { height } : undefined, tabIndex: -1, role: inline ? undefined : 'dialog', "aria-modal": inline ? undefined : true, "aria-labelledby": !inline && title ? titleId : undefined, "aria-label": !inline && !title && ariaLabel ? ariaLabel : undefined, children: [!useTitle && (headerActions || onClose) ? (_jsxs("div", { className: "popup-close-float", children: [headerActions, onClose && _jsx(Button, { size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, onClick: closeWithAnimation, disabled: closeDisabled, ariaLabel: t('common.close', '닫기') })] })) : (title || headerActions || onClose) ? (_jsxs("div", { className: `popup-header${subtitle ? ' popup-header--with-subtitle' : ''}`, children: [_jsxs("div", { className: "popup-header-main", children: [_jsx("span", { id: titleId, className: "popup-title popup-title--left h2", children: title }), subtitle && _jsx("span", { className: "popup-subtitle body-3", children: subtitle })] }), _jsxs("div", { className: "popup-header-actions", children: [headerActions, onClose && (_jsx(Button, { size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, onClick: closeWithAnimation, disabled: closeDisabled, ariaLabel: t('common.close', '닫기') }))] })] })) : null, _jsx("div", { className: "popup-content", ref: contentRef, tabIndex: contentScrollable ? 0 : undefined, children: _jsx("div", { className: "popup-content__inner", children: typeof children === 'function' ? children({ animateThen, closeWithAnimation }) : children }) }), buttons?.length ? (_jsx("div", { className: "popup-footer popup-footer--buttons", children: buttons.map((btn, i) => (_jsx(Button, { size: 48, variant: btn.variant ?? 'default', label: btn.label, disabled: btn.disabled, onClick: () => btn.onClick?.({ animateThen, closeWithAnimation }) }, i))) })) : footer ? (_jsx("div", { className: `popup-footer${footerClassName ? ` ${footerClassName}` : ''}`, children: typeof footer === 'function' ? footer({ animateThen, closeWithAnimation }) : footer })) : null] }) }));
|
|
164
|
+
style: height != null ? { height } : undefined, tabIndex: -1, role: inline ? undefined : 'dialog', "aria-modal": inline ? undefined : true, "aria-labelledby": !inline && title ? titleId : undefined, "aria-label": !inline && !title && ariaLabel ? ariaLabel : undefined, children: [!useTitle && (headerActions || onClose) ? (_jsxs("div", { className: "popup-close-float", children: [headerActions, onClose && _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, onClick: closeWithAnimation, disabled: closeDisabled, ariaLabel: t('common.close', '닫기') })] })) : (title || headerActions || onClose) ? (_jsxs("div", { className: `popup-header${subtitle ? ' popup-header--with-subtitle' : ''}`, children: [_jsxs("div", { className: "popup-header-main", children: [_jsx("span", { id: titleId, className: "popup-title popup-title--left h2", children: title }), subtitle && _jsx("span", { className: "popup-subtitle body-3", children: subtitle })] }), _jsxs("div", { className: "popup-header-actions", children: [headerActions, onClose && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, onClick: closeWithAnimation, disabled: closeDisabled, ariaLabel: t('common.close', '닫기') }))] })] })) : null, _jsx("div", { className: "popup-content", ref: contentRef, tabIndex: contentScrollable ? 0 : undefined, children: _jsx("div", { className: "popup-content__inner", children: typeof children === 'function' ? children({ animateThen, closeWithAnimation }) : children }) }), buttons?.length ? (_jsx("div", { className: "popup-footer popup-footer--buttons", children: buttons.map((btn, i) => (_jsx(Button, { htmlType: "button", size: 48, variant: btn.variant ?? 'default', label: btn.label, disabled: btn.disabled, onClick: () => btn.onClick?.({ animateThen, closeWithAnimation }) }, i))) })) : footer ? (_jsx("div", { className: `popup-footer${footerClassName ? ` ${footerClassName}` : ''}`, children: typeof footer === 'function' ? footer({ animateThen, closeWithAnimation }) : footer })) : null] }) }));
|
|
153
165
|
if (inline || typeof document === 'undefined')
|
|
154
166
|
return el;
|
|
155
167
|
return createPortal(el, document.body);
|
|
@@ -46,7 +46,7 @@ export default function SaveTablePopup({ title, saveLabel, defaultName = '', def
|
|
|
46
46
|
if (name.trim() && !isDuplicateName)
|
|
47
47
|
onSave(name.trim(), description.trim() || undefined);
|
|
48
48
|
}, [name, description, isDuplicateName, onSave]);
|
|
49
|
-
return (_jsx(Popup, { className: "popup--save-table", title: title ?? t('wordTable.save.title', '단어표 저장하기'), onClose: onCancel, footer: ({ animateThen, closeWithAnimation }) => (_jsxs(_Fragment, { children: [_jsx(Button, { size: 48, type: "bar", label: t('common.cancel', '취소'), onClick: closeWithAnimation }), _jsx(Button, { size: 48, type: "bar", variant: "main", label: saveLabel ?? t('common.save', '저장'), disabled: !name.trim() || isDuplicateName, onClick: () => handleSave(animateThen) })] })), children: _jsxs("div", { className: "save-table-popup__body", children: [preview && (_jsx("div", { ref: containerRef, className: "save-table-popup__preview-wrap", style: { height: previewState.fixedH }, children: _jsx("div", { className: "save-table-popup__preview", style: {
|
|
49
|
+
return (_jsx(Popup, { className: "popup--save-table", title: title ?? t('wordTable.save.title', '단어표 저장하기'), onClose: onCancel, footer: ({ animateThen, closeWithAnimation }) => (_jsxs(_Fragment, { children: [_jsx(Button, { htmlType: "button", size: 48, type: "bar", label: t('common.cancel', '취소'), onClick: closeWithAnimation }), _jsx(Button, { htmlType: "button", size: 48, type: "bar", variant: "main", label: saveLabel ?? t('common.save', '저장'), disabled: !name.trim() || isDuplicateName, onClick: () => handleSave(animateThen) })] })), children: _jsxs("div", { className: "save-table-popup__body", children: [preview && (_jsx("div", { ref: containerRef, className: "save-table-popup__preview-wrap", style: { height: previewState.fixedH }, children: _jsx("div", { className: "save-table-popup__preview", style: {
|
|
50
50
|
width: previewW, height: previewH,
|
|
51
51
|
transform: `scale(${previewState.scale})`,
|
|
52
52
|
top: previewState.offsetY,
|
|
@@ -101,5 +101,5 @@ export default function TrainingResultPopup(props) {
|
|
|
101
101
|
/today · /training · /my-word-training · /my-homework). 화면에 보이는 그
|
|
102
102
|
제목을 그대로 접근명으로 쓴다. 새 문구를 만들면 눈으로 보는 이름과 읽어
|
|
103
103
|
주는 이름이 갈라진다. */
|
|
104
|
-
ariaLabel: t('training.result.title', '훈련을 모두 마쳤습니다.'), className: "popup--training-result", footerClassName: "trp-result__foot", footer: ({ animateThen, closeWithAnimation }) => (_jsxs("div", { className: "trp-actions", children: [_jsx(Button, { size: 56, type: "bar", label: t('training.result.replay', '다시하기'), onClick: () => animateThen(onReplay) }), _jsx(Button, { size: 56, type: "bar", label: t('training.result.done', '완료'), variant: "main", onClick: closeWithAnimation })] })), children: () => (_jsxs("div", { className: "trp-result is-on", children: [_jsxs("header", { className: "trp-result__head", children: [_jsx("p", { className: "trp-result__eyebrow caption-1", children: t('training.result.eyebrow', '훈련 완료') }), _jsx("h1", { className: "trp-result__title h2", children: t('training.result.title', '훈련을 모두 마쳤습니다.') }), subtitle && _jsx("p", { className: "trp-result__subtitle body-3", children: subtitle }), _jsx("div", { className: "trp-score", children: _jsxs("div", { className: "trp-score__figure", "aria-label": t('training.result.scoreAria', { correct, total }), children: [_jsx("span", { className: "trp-score__big", children: correct }), _jsx("span", { className: "trp-score__sep", children: "/" }), _jsx("span", { className: "trp-score__total", children: total })] }) })] }), rows.length > 0 && (_jsx("section", { className: "trp-result__body", children: _jsxs("div", { className: `result-table${hasAnswerCols ? ' result-table--answer' : ''}`, role: "table", "aria-label": t('training.result.tableAriaLabel', '문항별 판정'), children: [_jsxs("div", { className: "result-table__head caption-1", role: "row", children: [_jsx("span", { role: "columnheader", children: t('common.col.problem', '문제') }), hasAnswerCols && _jsx("span", { role: "columnheader", children: t('training.result.colAnswerSelected', '정답 · 선택') }), _jsx("span", { className: "col-right", role: "columnheader", children: t('training.result.colJudge', '판정') })] }), rows.map((r, i) => (_jsxs("div", { className: "result-table__row", role: "row", children: [_jsxs("div", { className: "trp-row__problem", children: [_jsx("span", { className: "trp-row__no caption-1", children: String(i + 1).padStart(2, '0') }), _jsx("div", { className: "trp-row__q body-2", children: r.problem })] }), hasAnswerCols && (_jsxs("div", { className: "trp-row__answers", children: [r.answer !== null && (_jsxs("div", { className: "trp-row__answer-line", children: [_jsx("span", { className: "trp-row__answer-label answer-correct caption-1", children: t('training.result.colAnswer', '정답') }), _jsx("span", { className: "trp-row__answer-value body-2", children: r.answer })] })), _jsxs("div", { className: "trp-row__answer-line", children: [_jsx("span", { className: `trp-row__answer-label answer-selected caption-1 ${r.isCorrect ? 'is-correct' : 'is-wrong'}`, children: t('training.result.colSelected', '선택') }), _jsx("span", { className: `trp-row__answer-value body-2 ${r.isCorrect ? 'is-correct' : 'is-wrong'}`, children: r.selectedText ?? '—' })] })] })), _jsx("div", { className: "trp-row__judge", children: _jsx(Badge, { variant: r.variant, label: r.label, fill: "soft", minWidth: 80 }) })] }, i)))] }) }))] })) }));
|
|
104
|
+
ariaLabel: t('training.result.title', '훈련을 모두 마쳤습니다.'), className: "popup--training-result", footerClassName: "trp-result__foot", footer: ({ animateThen, closeWithAnimation }) => (_jsxs("div", { className: "trp-actions", children: [_jsx(Button, { htmlType: "button", size: 56, type: "bar", label: t('training.result.replay', '다시하기'), onClick: () => animateThen(onReplay) }), _jsx(Button, { htmlType: "button", size: 56, type: "bar", label: t('training.result.done', '완료'), variant: "main", onClick: closeWithAnimation })] })), children: () => (_jsxs("div", { className: "trp-result is-on", children: [_jsxs("header", { className: "trp-result__head", children: [_jsx("p", { className: "trp-result__eyebrow caption-1", children: t('training.result.eyebrow', '훈련 완료') }), _jsx("h1", { className: "trp-result__title h2", children: t('training.result.title', '훈련을 모두 마쳤습니다.') }), subtitle && _jsx("p", { className: "trp-result__subtitle body-3", children: subtitle }), _jsx("div", { className: "trp-score", children: _jsxs("div", { className: "trp-score__figure", "aria-label": t('training.result.scoreAria', { correct, total }), children: [_jsx("span", { className: "trp-score__big", children: correct }), _jsx("span", { className: "trp-score__sep", children: "/" }), _jsx("span", { className: "trp-score__total", children: total })] }) })] }), rows.length > 0 && (_jsx("section", { className: "trp-result__body", children: _jsxs("div", { className: `result-table${hasAnswerCols ? ' result-table--answer' : ''}`, role: "table", "aria-label": t('training.result.tableAriaLabel', '문항별 판정'), children: [_jsxs("div", { className: "result-table__head caption-1", role: "row", children: [_jsx("span", { role: "columnheader", children: t('common.col.problem', '문제') }), hasAnswerCols && _jsx("span", { role: "columnheader", children: t('training.result.colAnswerSelected', '정답 · 선택') }), _jsx("span", { className: "col-right", role: "columnheader", children: t('training.result.colJudge', '판정') })] }), rows.map((r, i) => (_jsxs("div", { className: "result-table__row", role: "row", children: [_jsxs("div", { className: "trp-row__problem", children: [_jsx("span", { className: "trp-row__no caption-1", children: String(i + 1).padStart(2, '0') }), _jsx("div", { className: "trp-row__q body-2", children: r.problem })] }), hasAnswerCols && (_jsxs("div", { className: "trp-row__answers", children: [r.answer !== null && (_jsxs("div", { className: "trp-row__answer-line", children: [_jsx("span", { className: "trp-row__answer-label answer-correct caption-1", children: t('training.result.colAnswer', '정답') }), _jsx("span", { className: "trp-row__answer-value body-2", children: r.answer })] })), _jsxs("div", { className: "trp-row__answer-line", children: [_jsx("span", { className: `trp-row__answer-label answer-selected caption-1 ${r.isCorrect ? 'is-correct' : 'is-wrong'}`, children: t('training.result.colSelected', '선택') }), _jsx("span", { className: `trp-row__answer-value body-2 ${r.isCorrect ? 'is-correct' : 'is-wrong'}`, children: r.selectedText ?? '—' })] })] })), _jsx("div", { className: "trp-row__judge", children: _jsx(Badge, { variant: r.variant, label: r.label, fill: "soft", minWidth: 80 }) })] }, i)))] }) }))] })) }));
|
|
105
105
|
}
|
|
@@ -71,8 +71,8 @@ export default function WordDataUpdatePopup({ title, progress, onDone, onRetry,
|
|
|
71
71
|
/* 오류인데 다시 시도할 곳이 없으면 '다시 시도' 가 아무 일도 안 하고, 이 팝업은 X 도
|
|
72
72
|
Esc 도 없어 **빠져나갈 길이 없었다**. 그때만 닫기 단추로 바꾼다. */
|
|
73
73
|
isError && onRetry
|
|
74
|
-
? _jsx(Button, { size: 56, variant: "main", onClick: onRetry, children: t('common.retry', '다시 시도') })
|
|
74
|
+
? _jsx(Button, { htmlType: "button", size: 56, variant: "main", onClick: onRetry, children: t('common.retry', '다시 시도') })
|
|
75
75
|
: isError
|
|
76
|
-
? _jsx(Button, { size: 56, variant: "main", onClick: onClose ?? onDone, children: t('common.close', '닫기') })
|
|
77
|
-
: _jsx(Button, { size: 56, variant: "main", disabled: !isDone, onClick: onDone, children: t('common.done', '완료') }), children: _jsxs("div", { className: "wdup-cards", children: [_jsx(PhaseCard, { icon: _jsx(Database, { size: 22, color: "var(--color-main, #29b6c8)", strokeWidth: 1.8 }), label: t('wordData.phaseWordDownload', '단어 데이터 다운로드'), state: wordState, subText: wordState === 'active' ? t('wordData.downloadingCount', { n: formatCount(wordCount) }) : undefined }), _jsx(PhaseCard, { icon: _jsx(ImageIcon, { size: 22, color: "var(--color-main, #29b6c8)", strokeWidth: 1.8 }), label: t('wordData.phaseImageCheck', '이미지 확인'), state: imgState, pct: imgPct, subText: imgSub }), _jsx(PhaseCard, { icon: _jsx(GitMerge, { size: 22, color: "var(--color-main, #29b6c8)", strokeWidth: 1.8 }), label: t('wordData.phasePairAnalysis', '단어쌍 분석'), state: pairState, pct: pairState === 'active' ? (pairPercent ?? 0) : undefined, subText: undefined })] }) }));
|
|
76
|
+
? _jsx(Button, { htmlType: "button", size: 56, variant: "main", onClick: onClose ?? onDone, children: t('common.close', '닫기') })
|
|
77
|
+
: _jsx(Button, { htmlType: "button", size: 56, variant: "main", disabled: !isDone, onClick: onDone, children: t('common.done', '완료') }), children: _jsxs("div", { className: "wdup-cards", children: [_jsx(PhaseCard, { icon: _jsx(Database, { size: 22, color: "var(--color-main, #29b6c8)", strokeWidth: 1.8 }), label: t('wordData.phaseWordDownload', '단어 데이터 다운로드'), state: wordState, subText: wordState === 'active' ? t('wordData.downloadingCount', { n: formatCount(wordCount) }) : undefined }), _jsx(PhaseCard, { icon: _jsx(ImageIcon, { size: 22, color: "var(--color-main, #29b6c8)", strokeWidth: 1.8 }), label: t('wordData.phaseImageCheck', '이미지 확인'), state: imgState, pct: imgPct, subText: imgSub }), _jsx(PhaseCard, { icon: _jsx(GitMerge, { size: 22, color: "var(--color-main, #29b6c8)", strokeWidth: 1.8 }), label: t('wordData.phasePairAnalysis', '단어쌍 분석'), state: pairState, pct: pairState === 'active' ? (pairPercent ?? 0) : undefined, subText: undefined })] }) }));
|
|
78
78
|
}
|
|
@@ -54,6 +54,6 @@ export default function SegmentedControl(props) {
|
|
|
54
54
|
};
|
|
55
55
|
/* Tab 자리를 받을 한 칸 — 고른 것이 있으면 그것, 없으면 고를 수 있는 첫 칸 */
|
|
56
56
|
const 탭자리 = 고를수있는.some((o) => o.key === value) ? value : 고를수있는[0]?.key;
|
|
57
|
-
return (_jsx("div", { ref: 그룹, onKeyDown: 키, className: cx('segmented-control', `segmented-control--${shape}`, `segmented-control--${size}`, fullWidth && 'segmented-control--full', !useBG && 'segmented-control--no-bg', className), role: "radiogroup", "aria-label": ariaLabel ?? t('segmentedControl.ariaLabel', '선택 탭'), children: options.map((opt) => (_jsx(Button, { role: "radio", "aria-checked": opt.key === value, "data-segkey": opt.key, tabIndex: opt.key === 탭자리 ? 0 : -1, label: opt.label, count: opt.count, disabled: opt.disabled, className: opt.key === value ? 'is-selected' : '', onClick: () => { if (!opt.disabled)
|
|
57
|
+
return (_jsx("div", { ref: 그룹, onKeyDown: 키, className: cx('segmented-control', `segmented-control--${shape}`, `segmented-control--${size}`, fullWidth && 'segmented-control--full', !useBG && 'segmented-control--no-bg', className), role: "radiogroup", "aria-label": ariaLabel ?? t('segmentedControl.ariaLabel', '선택 탭'), children: options.map((opt) => (_jsx(Button, { htmlType: "button", role: "radio", "aria-checked": opt.key === value, "data-segkey": opt.key, tabIndex: opt.key === 탭자리 ? 0 : -1, label: opt.label, count: opt.count, disabled: opt.disabled, className: opt.key === value ? 'is-selected' : '', onClick: () => { if (!opt.disabled)
|
|
58
58
|
onChange(opt.key); }, size: size, shape: shape, useBorder: false, useBG: false }, opt.key))) }));
|
|
59
59
|
}
|
|
@@ -24,7 +24,7 @@ function SideNavFooter(props) {
|
|
|
24
24
|
// 하나도 없으면 바닥 자체를 비운다 — 빈 채로 두면 여백(margin-top + padding)만 남는다.
|
|
25
25
|
if (!그릴것)
|
|
26
26
|
return null;
|
|
27
|
-
return (_jsxs("div", { className: "side-nav__footer", children: [onDebug && (_jsx(Button, { size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "terminal", ariaLabel: t('nav.debug', '디버그'), onClick: onDebug, selected: activeKey === 'debug' })), onNotification && (_jsxs("span", { className: "side-nav__notification-wrap", children: [_jsx(Button, { size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "bell", ariaLabel: nameWithCount(t, t('nav.notifications', '알림'), unreadCount), onClick: onNotification, selected: activeKey === 'notifications' }), !!unreadCount && unreadCount > 0 && (_jsx(Badge, { type: "dot", number: Math.min(unreadCount, 99), size: 24, "aria-hidden": true }))] })), onSetting && (_jsx(Button, { size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "settings", ariaLabel: t('nav.settings', '설정'), onClick: onSetting, selected: activeKey === 'settings' })), onHelp && (_jsx(Button, { size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "message-circle-question-mark", ariaLabel: t('nav.help', '도움말'), onClick: onHelp, selected: activeKey === 'help' })), onAccount && (_jsx(Button, { size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "user-round", ariaLabel: t('nav.account', '계정'), onClick: onAccount, selected: activeKey === 'account' }))] }));
|
|
27
|
+
return (_jsxs("div", { className: "side-nav__footer", children: [onDebug && (_jsx(Button, { htmlType: "button", size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "terminal", ariaLabel: t('nav.debug', '디버그'), onClick: onDebug, selected: activeKey === 'debug' })), onNotification && (_jsxs("span", { className: "side-nav__notification-wrap", children: [_jsx(Button, { htmlType: "button", size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "bell", ariaLabel: nameWithCount(t, t('nav.notifications', '알림'), unreadCount), onClick: onNotification, selected: activeKey === 'notifications' }), !!unreadCount && unreadCount > 0 && (_jsx(Badge, { type: "dot", number: Math.min(unreadCount, 99), size: 24, "aria-hidden": true }))] })), onSetting && (_jsx(Button, { htmlType: "button", size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "settings", ariaLabel: t('nav.settings', '설정'), onClick: onSetting, selected: activeKey === 'settings' })), onHelp && (_jsx(Button, { htmlType: "button", size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "message-circle-question-mark", ariaLabel: t('nav.help', '도움말'), onClick: onHelp, selected: activeKey === 'help' })), onAccount && (_jsx(Button, { htmlType: "button", size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "user-round", ariaLabel: t('nav.account', '계정'), onClick: onAccount, selected: activeKey === 'account' }))] }));
|
|
28
28
|
}
|
|
29
29
|
function SideNavButton(props) {
|
|
30
30
|
const { t } = useTranslation();
|