@ihabdevteam/core 0.132.0 → 0.134.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 +129 -0
- package/dist/components.cjs.js +177 -177
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +2 -2
- package/dist/ihabdevteam-core.cjs.js +182 -182
- package/dist/ihabdevteam-core.d.ts +8 -13
- 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/Breadcrumb.js +5 -1
- 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/Dialog.js +32 -9
- 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.css +2 -0
- package/dist/packages/core/src/components/Header.js +4 -1
- package/dist/packages/core/src/components/HearingDeviceInfo.js +1 -1
- package/dist/packages/core/src/components/LandingHeader.js +18 -1
- package/dist/packages/core/src/components/PageTitle.js +1 -1
- package/dist/packages/core/src/components/Pagination.js +6 -3
- package/dist/packages/core/src/components/Panel.d.ts +1 -1
- package/dist/packages/core/src/components/Panel.js +4 -1
- package/dist/packages/core/src/components/PanelRow.js +3 -3
- package/dist/packages/core/src/components/PassCard/passCardProduct.js +6 -0
- package/dist/packages/core/src/components/PaymentPlan.d.ts +6 -0
- package/dist/packages/core/src/components/PaymentPlan.js +6 -4
- 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/PromoBanner.js +1 -1
- package/dist/packages/core/src/components/SegmentedControl.js +1 -1
- package/dist/packages/core/src/components/SideNav.js +2 -2
- package/dist/packages/core/src/components/SoundCompass.js +10 -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 +8 -3
- 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/Training/useNoiseFade.js +22 -1
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +9 -0
- 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 +6 -5
- 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/hooks/useWordTrainingFilter.js +9 -4
- package/dist/packages/core/src/hooks/wordTrainingFilterModel.d.ts +1 -1
- package/dist/packages/core/src/hooks/wordTrainingFilterModel.js +7 -2
- package/dist/packages/core/src/i18n/locales/en.json +11 -0
- package/dist/packages/core/src/i18n/locales/ko.json +11 -0
- 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/format.js +17 -1
- package/dist/packages/core/src/utils/koreanPronunciation.js +7 -0
- package/dist/packages/core/src/utils/koreanSyllable.js +2 -1
- package/dist/packages/core/src/utils/pressable.js +4 -0
- package/dist/packages/core/src/utils/savedWordTables.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/wordTrainingPool.d.ts +7 -0
- package/dist/packages/core/src/utils/wordTrainingPool.js +30 -0
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +55 -17
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +2 -2
- package/package.json +1 -1
|
@@ -1308,7 +1308,7 @@ export declare interface ContrastFilterPanelProps {
|
|
|
1308
1308
|
}
|
|
1309
1309
|
|
|
1310
1310
|
/** 모드 전환 시 composition의 초성/모음/종성을 대상 모드 값으로 변환. 종성 '없음'(NO_CODA)은 유지. */
|
|
1311
|
-
export declare function convertComposition(c: WordComposition, to: 'char' | 'pron', preferredCharJung?: string | null): WordComposition;
|
|
1311
|
+
export declare function convertComposition(c: WordComposition, to: 'char' | 'pron', preferredCharJung?: string | null, preferredCharJong?: string | null): WordComposition;
|
|
1312
1312
|
|
|
1313
1313
|
/**
|
|
1314
1314
|
* 텍스트를 클립보드에 복사. 성공 여부를 boolean으로 반환.
|
|
@@ -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.134.0";
|
|
1373
1373
|
|
|
1374
1374
|
/** @public */
|
|
1375
1375
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -3794,6 +3794,12 @@ export declare interface PaymentPlanTier {
|
|
|
3794
3794
|
* 구매 횟수가 제한되지 않는다(같은 프로모션이되 1회 제한 대상은 아닌 tier용).
|
|
3795
3795
|
*/
|
|
3796
3796
|
openPromo?: boolean;
|
|
3797
|
+
/**
|
|
3798
|
+
* **계정당 1회만 살 수 있는 tier**(오픈 특가 30일 등). 이미 산 것(`ownedCodes`)이면 숨기고 기본 tier 를 카드로 올린다.
|
|
3799
|
+
* 안 주면 예전처럼 라벨에 '오픈 특가' 가 있는지로 판정한다 — 그런데 라벨은 번역되므로 영어 화면에서는 판정이 실패해
|
|
3800
|
+
* 이미 산 특가 카드가 남고 정가 30일권을 살 길이 없어진다. 번역되는 라벨 대신 이 값을 넘긴다.
|
|
3801
|
+
*/
|
|
3802
|
+
onceOnly?: boolean;
|
|
3797
3803
|
[k: string]: unknown;
|
|
3798
3804
|
}
|
|
3799
3805
|
|
|
@@ -7883,17 +7889,6 @@ export declare function useDeviceManagement(): {
|
|
|
7883
7889
|
handleSelectFromList: (form: DeviceForm) => void;
|
|
7884
7890
|
};
|
|
7885
7891
|
|
|
7886
|
-
/**
|
|
7887
|
-
* 현재 화면의 제목을 `document.title`에 반영한다 — `'<제목> · <앱 이름>'`.
|
|
7888
|
-
*
|
|
7889
|
-
* SPA는 라우트를 옮겨도 문서 제목이 그대로라, 브라우저 탭·히스토리·북마크로 위치를
|
|
7890
|
-
* 구분할 수 없고 화면낭독기도 이동을 인지하지 못한다(WCAG 2.4.2).
|
|
7891
|
-
* 언마운트하면 이전 제목으로 되돌린다.
|
|
7892
|
-
*
|
|
7893
|
-
* @param title 페이지 제목. 비어 있거나 문자열이 아니면 아무것도 하지 않는다.
|
|
7894
|
-
*
|
|
7895
|
-
* @public
|
|
7896
|
-
*/
|
|
7897
7892
|
export declare function useDocumentTitle(title?: string | null): void;
|
|
7898
7893
|
|
|
7899
7894
|
/**
|
|
@@ -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
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
2
3
|
import './Breadcrumb.css';
|
|
3
4
|
import Icon from './Icon';
|
|
4
5
|
/** @public */
|
|
5
6
|
export default function Breadcrumb({ items, className = '' }) {
|
|
6
|
-
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
return (
|
|
9
|
+
/* 영어로 박혀 있어 한국어 화면에서도 낭독기가 "breadcrumb" 이라고 읽었다 — 사전을 탄다 */
|
|
10
|
+
_jsx("nav", { className: `breadcrumb ${className}`.trim(), "aria-label": t('breadcrumb.ariaLabel', '이동 경로'), children: _jsx("ol", { className: "breadcrumb__list", children: items.map((item, index) => {
|
|
7
11
|
const isLast = index === items.length - 1;
|
|
8
12
|
return (_jsxs("li", { className: "breadcrumb__item", children: [isLast ? (_jsx("span", { className: "breadcrumb__current body-3", "aria-current": "page", children: item.label })) : (_jsx("button", { type: "button", className: "breadcrumb__link body-3", onClick: item.onClick, children: item.label })), !isLast && (_jsx(Icon, { icon: "chevron-right", size: 14, className: "breadcrumb__separator" }))] }, index));
|
|
9
13
|
}) }) }));
|
|
@@ -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
|
}
|
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useId, useRef } from 'react';
|
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
6
|
import { cx } from '../utils/cx';
|
|
7
|
+
import { lockBodyScroll } from '../hooks/bodyScrollLock';
|
|
7
8
|
import { pushEsc } from '../hooks/escStack';
|
|
8
9
|
import { useFocusTrap } from '../hooks/useFocusTrap';
|
|
9
10
|
import './Dialog.css';
|
|
@@ -39,12 +40,18 @@ export default function Dialog({ open, onClose, ariaLabel, title, children, size
|
|
|
39
40
|
if (!open)
|
|
40
41
|
return;
|
|
41
42
|
const 돌아갈곳 = document.activeElement;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
/* 스크롤 잠금은 **공유 카운터**(Popup·TrainingPagePopup 과 같은 것)로 — 전에는 body.style.overflow 를 제 손으로
|
|
44
|
+
기억했다 되돌려, Dialog 위에 Popup 을 열고 Dialog 를 먼저 닫으면 Popup 이 떠 있는데 스크롤이 풀리고, 이어 Popup 을
|
|
45
|
+
닫으면 overflow:hidden 으로 영영 굳었다. */
|
|
46
|
+
const 풀기 = lockBodyScroll();
|
|
47
|
+
const 판 = surfaceRef.current;
|
|
48
|
+
판?.focus();
|
|
45
49
|
return () => {
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
풀기();
|
|
51
|
+
// 포커스가 이 창 안이거나 갈 곳을 잃었을 때만 되돌린다 — 이미 다른 창으로 옮겨 갔으면 빼앗지 않는다(Popup 과 같다)
|
|
52
|
+
const 지금 = document.activeElement;
|
|
53
|
+
if (!지금 || 지금 === document.body || (판 && 판.contains(지금)))
|
|
54
|
+
돌아갈곳?.focus?.();
|
|
48
55
|
};
|
|
49
56
|
}, [open]);
|
|
50
57
|
/* ESC 는 **공유 스택**에 맡긴다 — 예전에는 window 에서 직접 들었다.
|
|
@@ -53,11 +60,21 @@ export default function Dialog({ open, onClose, ariaLabel, title, children, size
|
|
|
53
60
|
**아래 팝업이 닫히고 이 확인창만 남는** 꼴이다 — 닫으려던 것은 그대로 있고
|
|
54
61
|
뒤에 있던 화면이 사라진다.
|
|
55
62
|
스택에 올리면 맨 위 하나만 닫힌다(그것이 이 창이다). */
|
|
63
|
+
/* 스택에는 **열릴 때 한 번** 올린다 — handleClose 가 바뀔 때마다(인라인 onClose) 다시 올리면 맨 위로 끼어들어, 위에
|
|
64
|
+
팝업이 떠 있어도 Esc 가 이 창을 닫았다. closeOnEscape={false} 라도 올려서 Esc 를 **삼킨다** — 안 올리면 이 창
|
|
65
|
+
아래에 가려진 팝업이 닫혔다(모달은 제 아래를 지킨다). */
|
|
66
|
+
const closeRef = useRef(handleClose);
|
|
67
|
+
const escRef = useRef(closeOnEscape);
|
|
68
|
+
useEffect(() => { closeRef.current = handleClose; escRef.current = closeOnEscape; });
|
|
56
69
|
useEffect(() => {
|
|
57
|
-
if (!open
|
|
70
|
+
if (!open)
|
|
58
71
|
return;
|
|
59
|
-
return pushEsc(
|
|
60
|
-
|
|
72
|
+
return pushEsc(() => { if (escRef.current)
|
|
73
|
+
closeRef.current(); });
|
|
74
|
+
}, [open]);
|
|
75
|
+
/* 바탕 누르기 — 누른 곳과 뗀 곳이 **둘 다 바탕일 때만** 닫는다(Popup 0.128.0 과 같다). 판 안에서 글자를 긁어
|
|
76
|
+
고르다 바깥에서 놓으면 click 이 공통 조상(바탕)에 떨어져 창이 닫혔다. */
|
|
77
|
+
const 바탕에서누름 = useRef(false);
|
|
61
78
|
/* Tab 을 이 판 안에 가둔다 — `aria-modal="true"` 는 **낭독기에게만** 통한다.
|
|
62
79
|
눈으로 보며 Tab 을 누르는 사람은 그 표시를 못 보므로, 가둠이 없으면 마지막
|
|
63
80
|
단추 다음에서 **뒤 화면**으로 빠져나간다. 실제로 그랬다: 놀이터
|
|
@@ -67,5 +84,11 @@ export default function Dialog({ open, onClose, ariaLabel, title, children, size
|
|
|
67
84
|
useFocusTrap(surfaceRef, open);
|
|
68
85
|
if (!open || typeof document === 'undefined')
|
|
69
86
|
return null;
|
|
70
|
-
return createPortal(_jsx("div", { className: cx('dialog', className),
|
|
87
|
+
return createPortal(_jsx("div", { className: cx('dialog', className), onPointerDown: closeOnBackdrop ? (e) => { 바탕에서누름.current = e.target === e.currentTarget; } : undefined, onMouseDown: (e) => { if (e.target === e.currentTarget)
|
|
88
|
+
e.preventDefault(); }, onClick: closeOnBackdrop ? (e) => {
|
|
89
|
+
const 눌렀나 = 바탕에서누름.current;
|
|
90
|
+
바탕에서누름.current = false;
|
|
91
|
+
if (눌렀나 && e.target === e.currentTarget)
|
|
92
|
+
handleClose();
|
|
93
|
+
} : undefined, children: _jsxs("div", { ref: surfaceRef, role: "dialog", "aria-modal": "true", "aria-label": title ? undefined : ariaLabel, "aria-labelledby": title ? titleId : undefined, tabIndex: -1, className: cx('dialog__surface', `dialog__surface--${size}`, contentClassName), onClick: (e) => e.stopPropagation(), children: [title ? (_jsx("div", { className: "dialog__header", children: _jsx("h2", { id: titleId, className: "dialog__title h4b", children: title }) })) : null, showClose ? (_jsx("button", { type: "button", className: "dialog__close", "aria-label": t('common.close', '닫기'), onClick: handleClose, children: _jsx(CloseIcon, {}) })) : null, _jsx("div", { className: "dialog__body", children: children })] }) }), document.body);
|
|
71
94
|
}
|
|
@@ -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
|
}
|
|
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
5
5
|
import { useMediaQuery } from '../hooks';
|
|
6
6
|
import BottomNav from './BottomNav';
|
|
7
7
|
import Button from './Button';
|
|
8
|
+
import Badge from './Badge';
|
|
8
9
|
import MenuTooltip from './Tooltip/MenuTooltip';
|
|
9
10
|
import { useAnchoredMenu } from '../hooks/useAnchoredMenu';
|
|
10
11
|
import { createPortal } from 'react-dom';
|
|
@@ -112,7 +113,9 @@ export default function Header({ activeKey, onChange, items, malgwiItems: _malgw
|
|
|
112
113
|
onClick: () => { it.onClick?.(); closeUtilMenu(); },
|
|
113
114
|
})),
|
|
114
115
|
}], [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:
|
|
116
|
+
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: onNotification && unreadCount && unreadCount > 0
|
|
117
|
+
? t('nav.badgeLabel', { label: t('header.menuOpen', '메뉴 열기'), count: unreadCount, defaultValue: '{{label}}, {{count}}개' })
|
|
118
|
+
: 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, { 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
119
|
}
|
|
117
120
|
/**
|
|
118
121
|
* `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
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useId, useState } from 'react';
|
|
3
|
+
import { useEffect, useId, useState } from 'react';
|
|
4
|
+
import { pushEsc } from '../hooks/escStack';
|
|
4
5
|
import './LandingHeader.css';
|
|
5
6
|
/**
|
|
6
7
|
* 랜딩/마케팅 페이지용 상단 고정 헤더.
|
|
@@ -22,6 +23,22 @@ export default function LandingHeader({ logo, logoAlt = '', logoHref, links = []
|
|
|
22
23
|
// 서랍 id 를 고정 문자열로 두면 한 화면에 헤더가 둘일 때(미리보기·놀이터 견본) id 가 겹쳐
|
|
23
24
|
// 햄버거의 aria-controls 가 엉뚱한 서랍을 가리킨다 — 헤더마다 따로 짓는다.
|
|
24
25
|
const drawerId = useId();
|
|
26
|
+
/* 서랍이 열려 있으면 Esc 로 닫는다(공유 ESC 스택 — 맨 위 하나만). 넓은 화면(961px~, 서랍이 CSS 로 숨는 폭)으로 가면
|
|
27
|
+
닫는다 — 안 닫으면 open 이 남아 좁은 폭으로 돌아오는 순간 서랍이 다시 펼쳐졌다. */
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!open)
|
|
30
|
+
return;
|
|
31
|
+
const 풀기 = pushEsc(() => setOpen(false));
|
|
32
|
+
const mq = typeof window !== 'undefined' && window.matchMedia ? window.matchMedia('(min-width: 961px)') : null;
|
|
33
|
+
const 넓어짐 = (e) => { if (e.matches)
|
|
34
|
+
setOpen(false); };
|
|
35
|
+
if (mq) {
|
|
36
|
+
if (mq.matches)
|
|
37
|
+
setOpen(false);
|
|
38
|
+
mq.addEventListener?.('change', 넓어짐);
|
|
39
|
+
}
|
|
40
|
+
return () => { 풀기(); mq?.removeEventListener?.('change', 넓어짐); };
|
|
41
|
+
}, [open]);
|
|
25
42
|
const innerClass = containerClassName
|
|
26
43
|
? `${containerClassName} landing-header__inner`
|
|
27
44
|
: 'landing-header__inner';
|
|
@@ -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
|
}
|
|
@@ -14,7 +14,10 @@ export default function Pagination({ page, totalPages, onChange, prevLabel, next
|
|
|
14
14
|
const { t } = useTranslation();
|
|
15
15
|
const resolvedPrev = prevLabel ?? t('common.prevPage', '이전 페이지');
|
|
16
16
|
const resolvedNext = nextLabel ?? t('common.nextPage', '다음 페이지');
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
/* 범위 밖 쪽(page > totalPages — 결과가 줄어든 직후)에서도 '이전' 이 범위 안으로 돌아오게 먼저 조인다.
|
|
18
|
+
전에는 page-1 이 여전히 totalPages 보다 커 눌러도 범위 밖에 머물렀다. */
|
|
19
|
+
const 마지막 = Math.max(1, totalPages);
|
|
20
|
+
const handlePrev = () => onChange(Math.max(1, Math.min(마지막, page - 1)));
|
|
21
|
+
const handleNext = () => onChange(Math.min(마지막, page + 1));
|
|
22
|
+
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
23
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { cx } from '../utils/cx';
|
|
4
4
|
import { PanelRow } from './PanelRow';
|
|
5
|
+
import { Children } from 'react';
|
|
5
6
|
import Icon from './Icon';
|
|
6
7
|
import './Panel.css';
|
|
7
8
|
export default function Panel({ title, icon, disc, actions, size = 'medium', minHeight, maxWidth, className, scrollable, useBorder = true, useBg = true, useRowBorder = true, labelWidth, form = false, loading, empty, children }) {
|
|
@@ -15,7 +16,9 @@ export default function Panel({ title, icon, disc, actions, size = 'medium', min
|
|
|
15
16
|
style.maxWidth = typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth;
|
|
16
17
|
if (labelWidth !== undefined)
|
|
17
18
|
style['--panel-label-width'] = typeof labelWidth === 'number' ? `${labelWidth}px` : labelWidth;
|
|
18
|
-
return (_jsxs("div", { className: cls, style: Object.keys(style).length ? style : undefined, children: [(title != null || disc != null) && (_jsxs("div", { className: cx('panel__header', disc != null ? 'panel__header--with-disc' : ''), children: [icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "panel__icon" })), _jsxs("div", { className: "panel__title-group", children: [title != null && (_jsx("h2", { className: `panel__title ${disc != null ? 'body-3b' : 'h5b'}`, children: title })), disc != null && _jsx("p", { className: "panel__disc caption-1", children: disc })] }), actions && _jsx("div", { className: "panel__actions", children: actions })] })), loading ? (_jsx(PanelRow, { variant: "loading" })
|
|
19
|
+
return (_jsxs("div", { className: cls, style: Object.keys(style).length ? style : undefined, children: [(title != null || disc != null) && (_jsxs("div", { className: cx('panel__header', disc != null ? 'panel__header--with-disc' : ''), children: [icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "panel__icon" })), _jsxs("div", { className: "panel__title-group", children: [title != null && (_jsx("h2", { className: `panel__title ${disc != null ? 'body-3b' : 'h5b'}`, children: title })), disc != null && _jsx("p", { className: "panel__disc caption-1", children: disc })] }), actions && _jsx("div", { className: "panel__actions", children: actions })] })), loading ? (_jsx(PanelRow, { variant: "loading" })
|
|
20
|
+
/* 비었는지는 React 가 그리는 것으로 센다 — `{조건 && 행들}` 이 false 면 children 이 false 라 빈 상태 문구가 안 떴다 */
|
|
21
|
+
) : empty != null && Children.toArray(children).length === 0 ? (_jsx(PanelRow, { variant: "empty", children: empty })) : scrollable ? (_jsx("div", { className: "panel__body--scroll", children: children })) : children] }));
|
|
19
22
|
}
|
|
20
23
|
// ── PanelGrid ──────────────────────────────────────────────────────────────
|
|
21
24
|
/** @public */
|
|
@@ -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
|
}
|
|
@@ -24,6 +24,12 @@ function derivePlan(p) {
|
|
|
24
24
|
const nm = name.match(/(\d+\s*(?:년|개월|주|일))\s*(?:이용권)?\s*$/);
|
|
25
25
|
if (nm)
|
|
26
26
|
return nm[1].replace(/\s+/g, '');
|
|
27
|
+
/* 영어 이름('Open Special 30-Day Plan'·'1-Year Plan')에서도 일·년 기간을 뽑는다 — 한국어 꼴만 봐서 영어 화면(말귀 랜딩
|
|
28
|
+
요금 카드는 code 를 안 넘긴다)에서는 이름 전체가 큰 글씨로 찍히고 뒤에 등급 이름이 또 붙었다. 돌려주는 토큰은 위와 같은
|
|
29
|
+
'30일'·'1년' 꼴이다 — PassCard 가 숫자와 단위로 쪼개 단위를 passCard.unitDay·unitYear 로 번역한다. */
|
|
30
|
+
const en = name.match(/(\d+)\s*-?\s*(day|year)s?\b/i);
|
|
31
|
+
if (en)
|
|
32
|
+
return en[2].toLowerCase() === 'year' ? `${en[1]}년` : `${en[1]}일`;
|
|
27
33
|
return name || '이용권';
|
|
28
34
|
}
|
|
29
35
|
/**
|