@ihabdevteam/core 0.131.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 +108 -0
- package/dist/components.cjs.js +178 -178
- package/dist/components.esm.css +2 -2
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +181 -181
- package/dist/ihabdevteam-core.d.ts +55 -17
- package/dist/packages/core/src/components/Accordion.js +4 -1
- package/dist/packages/core/src/components/Badge.d.ts +5 -0
- package/dist/packages/core/src/components/Badge.js +3 -3
- package/dist/packages/core/src/components/BarChart.d.ts +1 -1
- package/dist/packages/core/src/components/BarChart.js +25 -14
- 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/Calendar.js +4 -0
- package/dist/packages/core/src/components/ChartSection.css +16 -0
- package/dist/packages/core/src/components/ChartSection.d.ts +8 -1
- package/dist/packages/core/src/components/ChartSection.js +2 -2
- 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.css +6 -0
- package/dist/packages/core/src/components/PageTitle.d.ts +9 -1
- package/dist/packages/core/src/components/PageTitle.js +3 -3
- 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.d.ts +8 -0
- package/dist/packages/core/src/components/Popup/Popup.js +17 -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/RingChart.d.ts +3 -1
- package/dist/packages/core/src/components/RingChart.js +2 -2
- package/dist/packages/core/src/components/SegmentedControl.js +1 -1
- package/dist/packages/core/src/components/SideNav.js +27 -10
- package/dist/packages/core/src/components/Table.d.ts +17 -1
- package/dist/packages/core/src/components/Table.js +15 -9
- package/dist/packages/core/src/components/Tabs.js +7 -2
- package/dist/packages/core/src/components/TextField.js +34 -5
- 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/barChartTypes.d.ts +2 -0
- package/dist/packages/core/src/components/useComboboxSuggestions.d.ts +1 -0
- package/dist/packages/core/src/components/useComboboxSuggestions.js +12 -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
|
@@ -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
|
}
|
|
@@ -32,3 +32,9 @@
|
|
|
32
32
|
음수 여백으로 왼쪽을 당겨, 아이콘 단추의 제 여백 때문에 제목이 페이지의 다른
|
|
33
33
|
제목들보다 오른쪽으로 밀려 보이는 것을 막는다(글자 시작선을 맞춘다). */
|
|
34
34
|
.page-title__back { margin-left: -8px; margin-right: -4px; flex-shrink: 0; }
|
|
35
|
+
|
|
36
|
+
/* wrap — 오른쪽이 넓으면 줄을 접는다. 제목 칸의 바닥을 320px 로 두되 **제 그릇보다 크지 않게**(min) —
|
|
37
|
+
그냥 320px 이면 좁은 화면(세로 배치, 그릇 280px)에서 제목이 그릇을 넘쳐 오른쪽이 잘린다
|
|
38
|
+
(ChartSection 의 min-width 320px 가 똑같이 그랬다). */
|
|
39
|
+
.page-title--wrap { flex-wrap: wrap; }
|
|
40
|
+
.page-title--wrap .page-title__main { min-width: min(320px, 100%); }
|
|
@@ -29,7 +29,15 @@ type PageTitleProps = {
|
|
|
29
29
|
* title 이 문자열이면 이 값은 필요 없다.
|
|
30
30
|
*/
|
|
31
31
|
documentTitle?: string;
|
|
32
|
+
/**
|
|
33
|
+
* 오른쪽(`rightAction`)이 넓어 제목 칸을 짓누를 때 **줄을 접어** 오른쪽을 아래로 내린다. 기본 false(한 줄 유지).
|
|
34
|
+
*
|
|
35
|
+
* 한 줄로 두면 제목 칸이 `min-width: 0` 까지 줄어, 탭 일곱 같은 넓은 오른쪽이 제목을 한 글자씩 세로로
|
|
36
|
+
* 밀어낸다(말귀 고객 상세 1135px 실측). 켜면 제목 칸에 320px(좁으면 제 폭) 바닥을 주고, 그 바닥이
|
|
37
|
+
* 안 들어가면 오른쪽이 다음 줄로 간다.
|
|
38
|
+
*/
|
|
39
|
+
wrap?: boolean;
|
|
32
40
|
};
|
|
33
41
|
/** @public */
|
|
34
|
-
export default function PageTitle({ title, sub, rightAction, titleAction, badge, badgeVariant, documentTitle, onBack, backLabel }: PageTitleProps): import("react").JSX.Element;
|
|
42
|
+
export default function PageTitle({ title, sub, rightAction, titleAction, badge, badgeVariant, documentTitle, onBack, backLabel, wrap }: PageTitleProps): import("react").JSX.Element;
|
|
35
43
|
export {};
|
|
@@ -5,15 +5,15 @@ import Badge from './Badge';
|
|
|
5
5
|
import Button from './Button';
|
|
6
6
|
import { useDocumentTitle } from '../hooks/useDocumentTitle';
|
|
7
7
|
/** @public */
|
|
8
|
-
export default function PageTitle({ title, sub, rightAction, titleAction, badge, badgeVariant = 'default', documentTitle, onBack, backLabel }) {
|
|
8
|
+
export default function PageTitle({ title, sub, rightAction, titleAction, badge, badgeVariant = 'default', documentTitle, onBack, backLabel, wrap = false }) {
|
|
9
9
|
const { t } = useTranslation();
|
|
10
10
|
// 페이지 제목이 곧 문서 제목이 되도록 맞춘다(WCAG 2.4.2).
|
|
11
11
|
useDocumentTitle(typeof title === 'string' ? title : (documentTitle ?? null));
|
|
12
12
|
const 뒤로이름 = backLabel ?? t('common.back', '뒤로');
|
|
13
|
-
return (_jsxs("div", { className:
|
|
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
|
}
|
|
@@ -73,6 +73,14 @@ export interface PopupProps {
|
|
|
73
73
|
* 더 넓거나 무제한이 필요하면 className으로 `.popup-panel` max-width를 직접 override.
|
|
74
74
|
*/
|
|
75
75
|
size?: 'small' | 'medium' | 'large';
|
|
76
|
+
/**
|
|
77
|
+
* 패널 높이를 못 박는다(숫자는 px, 문자열은 CSS 길이 그대로). 미지정 시 내용만큼(기본).
|
|
78
|
+
*
|
|
79
|
+
* 탭을 옮길 때마다 내용 높이가 달라 창이 위아래로 뛰는 팝업에 쓴다. **화면보다 크면 화면에
|
|
80
|
+
* 맞춘다** — 패널의 `max-height`(데스크톱 `100% - 60px`, 좁은 화면은 하단 내비 몫까지 뺀 값)가
|
|
81
|
+
* 그대로 걸리므로 창이 화면 밖으로 나가지 않는다. 넘치는 내용은 본문이 스스로 굴린다.
|
|
82
|
+
*/
|
|
83
|
+
height?: number | string;
|
|
76
84
|
/**
|
|
77
85
|
* 이 값이 바뀌면 본문(.popup-content) 스크롤을 맨 위로 되돌린다.
|
|
78
86
|
* 탭/모드 전환처럼 내부 콘텐츠가 통째로 바뀌는 팝업에서, 스크롤을 내린 채
|
|
@@ -13,7 +13,7 @@ const ANIM_MS = 260;
|
|
|
13
13
|
/** @public */
|
|
14
14
|
export default function Popup(props) {
|
|
15
15
|
const { t } = useTranslation();
|
|
16
|
-
const { title, subtitle, headerActions, children, buttons, footer, footerClassName = "", onClose, closeDisabled = false, beforeClose, closeOnBackdrop = true, className = "", ariaLabel, useTitle = true, inline = false, size, resetScrollKey } = props || {};
|
|
16
|
+
const { title, subtitle, headerActions, children, buttons, footer, footerClassName = "", onClose, closeDisabled = false, beforeClose, closeOnBackdrop = true, className = "", ariaLabel, useTitle = true, inline = false, size, height, resetScrollKey } = props || {};
|
|
17
17
|
const [closing, setClosing] = useState(false);
|
|
18
18
|
// 닫힘 애니메이션 중(260ms)엔 버튼이 시각적으로만 흐려질 뿐 클릭은 계속 가능해,
|
|
19
19
|
// 그 사이 같은 버튼을 연타하면 cb(확인/완료 콜백)가 여러 번 예약되어 중복 실행된다.
|
|
@@ -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,7 +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}` : ''}`,
|
|
163
|
+
/* max-height(CSS)가 height 보다 이기므로 화면보다 크게 줘도 화면에 맞춰진다 */
|
|
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] }) }));
|
|
151
165
|
if (inline || typeof document === 'undefined')
|
|
152
166
|
return el;
|
|
153
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
|
}
|
|
@@ -58,6 +58,8 @@ export interface RingChartProps {
|
|
|
58
58
|
loading?: boolean;
|
|
59
59
|
/** 섹션 루트에 추가할 클래스 (titleKey 지정 시에만 사용) */
|
|
60
60
|
className?: string;
|
|
61
|
+
/** CSS 격자 칸 안에 세운다 — 아래 여백·최소 폭을 풀고 링을 카드 바닥에 맞춘다. ChartSection `inGrid` 참고(titleKey 지정 시에만 사용) */
|
|
62
|
+
inGrid?: boolean;
|
|
61
63
|
}
|
|
62
64
|
/**
|
|
63
65
|
* 도넛 링 차트 — 전체 대비 완료 비율을 원형 호로 표현.
|
|
@@ -65,4 +67,4 @@ export interface RingChartProps {
|
|
|
65
67
|
* 미지정 시에는 헤더 없는 순수 링만 렌더된다(크기/색상 데모 등).
|
|
66
68
|
* @public
|
|
67
69
|
*/
|
|
68
|
-
export default function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, ...ringProps }: RingChartProps): import("react").JSX.Element;
|
|
70
|
+
export default function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid, ...ringProps }: RingChartProps): import("react").JSX.Element;
|
|
@@ -40,7 +40,7 @@ function RingChartCanvas({ done, total, segments, color = 'var(--color-main)', s
|
|
|
40
40
|
* 미지정 시에는 헤더 없는 순수 링만 렌더된다(크기/색상 데모 등).
|
|
41
41
|
* @public
|
|
42
42
|
*/
|
|
43
|
-
export default function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, ...ringProps }) {
|
|
43
|
+
export default function RingChart({ titleKey, headRight, icon, showTotal, totalLabelKey, legend, loading, className, inGrid = false, ...ringProps }) {
|
|
44
44
|
const { t } = useTranslation();
|
|
45
45
|
if (!titleKey)
|
|
46
46
|
return _jsx(RingChartCanvas, { ...ringProps });
|
|
@@ -53,5 +53,5 @@ export default function RingChart({ titleKey, headRight, icon, showTotal, totalL
|
|
|
53
53
|
.filter(Boolean).join(' ') || undefined;
|
|
54
54
|
// StatChart(.manager-stat-card__icon)와 동일한 톤 — headRight 자리에 렌더(둘 중 하나만 사용)
|
|
55
55
|
const iconNode = icon && (_jsx("span", { className: "chart-section-icon", children: _jsx(Icon, { icon: icon, className: "chart-section-icon-inner" }) }));
|
|
56
|
-
return (_jsx(ChartSection, { className: sectionClassName, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: iconNode || (isSmall ? undefined : headRight), subRow: (showTotal || legend) && _jsxs(_Fragment, { children: [showTotal ? (_jsxs(ChartTotal, { label: !isSmall && totalLabelKey ? t(totalLabelKey) : undefined, children: [_jsx("span", { className: "barchart-total__num h0", children: pct }), _jsx("span", { className: "barchart-total__unit h5", children: "%" })] })) : _jsx("span", {}), legend && _jsx(ChartLegend, { items: legend })] }), children: loading ? (_jsx("div", { className: "manager-bar-chart-loading", children: _jsx(Loader, { size: 32 }) })) : (_jsx("div", { className: "ring-chart-section-body ring-chart-section-body--offset", children: _jsx(RingChartCanvas, { ...ringProps, hideCenter: isSmall || ringProps.hideCenter }) })) }));
|
|
56
|
+
return (_jsx(ChartSection, { className: sectionClassName, inGrid: inGrid, title: t(titleKey), titleClassName: isSmall ? 'h5 chart-section-title--small' : undefined, headRight: iconNode || (isSmall ? undefined : headRight), subRow: (showTotal || legend) && _jsxs(_Fragment, { children: [showTotal ? (_jsxs(ChartTotal, { label: !isSmall && totalLabelKey ? t(totalLabelKey) : undefined, children: [_jsx("span", { className: "barchart-total__num h0", children: pct }), _jsx("span", { className: "barchart-total__unit h5", children: "%" })] })) : _jsx("span", {}), legend && _jsx(ChartLegend, { items: legend })] }), children: loading ? (_jsx("div", { className: "manager-bar-chart-loading", children: _jsx(Loader, { size: 32 }) })) : (_jsx("div", { className: "ring-chart-section-body ring-chart-section-body--offset", children: _jsx(RingChartCanvas, { ...ringProps, hideCenter: isSmall || ringProps.hideCenter }) })) }));
|
|
57
57
|
}
|
|
@@ -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
|
}
|
|
@@ -6,6 +6,10 @@ import Button from './Button';
|
|
|
6
6
|
import Badge from './Badge';
|
|
7
7
|
import React, { useCallback } from 'react';
|
|
8
8
|
import { useTranslation } from 'react-i18next';
|
|
9
|
+
/** 단추 이름에 개수를 붙인다 — 뱃지는 aria-hidden 이라 개수는 이름으로만 전해진다(BottomNav 와 같은 문구). */
|
|
10
|
+
function nameWithCount(t, label, count) {
|
|
11
|
+
return count && count > 0 ? t('nav.badgeLabel', { label, count, defaultValue: '{{label}}, {{count}}개' }) : label;
|
|
12
|
+
}
|
|
9
13
|
function SideNavFooter(props) {
|
|
10
14
|
const { t } = useTranslation();
|
|
11
15
|
const { activeKey, unreadCount, onDebug, onNotification, onSetting, onHelp, onAccount } = props || {};
|
|
@@ -20,9 +24,10 @@ function SideNavFooter(props) {
|
|
|
20
24
|
// 하나도 없으면 바닥 자체를 비운다 — 빈 채로 두면 여백(margin-top + padding)만 남는다.
|
|
21
25
|
if (!그릴것)
|
|
22
26
|
return null;
|
|
23
|
-
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: t('nav.notifications', '알림'), onClick: onNotification, selected: activeKey === 'notifications' }), !!unreadCount && (_jsx(Badge, { type: "dot", number: Math.min(unreadCount, 99), size: 24 }))] })), 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' }))] }));
|
|
24
28
|
}
|
|
25
29
|
function SideNavButton(props) {
|
|
30
|
+
const { t } = useTranslation();
|
|
26
31
|
const { item, itemKey, label, icon, active, onClick, style, state, labelStyle } = props || {};
|
|
27
32
|
const key = item?.key ?? itemKey ?? '';
|
|
28
33
|
const text = (item?.label ?? label ?? '');
|
|
@@ -34,13 +39,19 @@ function SideNavButton(props) {
|
|
|
34
39
|
const handleClick = useCallback((e) => {
|
|
35
40
|
if (isDisabled)
|
|
36
41
|
return;
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
/* 포커스는 **포인터로 눌렀을 때만** 걷는다. 걷는 까닭은 마우스로 누른 뒤 :focus 틴트가 남고 태블릿
|
|
43
|
+
레일이 :focus-within 으로 펼친 채 남기 때문인데, 키보드로 Enter 를 누른 사람까지 걷으면 포커스가
|
|
44
|
+
body 로 떨어져 메뉴 처음부터 Tab 을 다시 밟아야 했다. 키보드가 낸 click 은 detail 이 0 이다. */
|
|
45
|
+
const fromPointer = e.detail > 0;
|
|
46
|
+
if (fromPointer) {
|
|
47
|
+
try {
|
|
48
|
+
e.currentTarget.blur();
|
|
49
|
+
}
|
|
50
|
+
catch (err) { }
|
|
39
51
|
}
|
|
40
|
-
|
|
41
|
-
onClick?.(key, e.currentTarget);
|
|
52
|
+
onClick?.(key, e.currentTarget, fromPointer);
|
|
42
53
|
}, [isDisabled, key, onClick]);
|
|
43
|
-
return (_jsxs("button", { className: btnClass, onClick: handleClick, "aria-label": typeof text === 'string' ? text : undefined, style: style, "aria-disabled": isDisabled, children: [iconElement, _jsx("span", { className: `side-nav__label button-1 f`, style: labelStyle, children: text }), badgeCount > 0 && (_jsx(Badge, { type: "dot", position: "inline", number: Math.min(badgeCount, 99), size: 24, "aria-
|
|
54
|
+
return (_jsxs("button", { type: "button", className: btnClass, onClick: handleClick, "aria-label": typeof text === 'string' ? nameWithCount(t, text, badgeCount) : undefined, style: style, "aria-disabled": isDisabled, children: [iconElement, _jsx("span", { className: `side-nav__label button-1 f`, style: labelStyle, children: text }), badgeCount > 0 && (_jsx(Badge, { type: "dot", position: "inline", number: Math.min(badgeCount, 99), size: 24, "aria-hidden": true })), state === 'loading' && (_jsx("span", { className: "side-nav__spinner", "aria-label": "loading" }))] }));
|
|
44
55
|
}
|
|
45
56
|
/** @public */
|
|
46
57
|
export default function SideNav(props) {
|
|
@@ -49,15 +60,17 @@ export default function SideNav(props) {
|
|
|
49
60
|
const handleModeSwitch = useCallback(() => onAppModeChange?.(appMode === 'malgwi' ? 'manager' : 'malgwi'), [onAppModeChange, appMode]);
|
|
50
61
|
return (_jsxs("nav", { "aria-label": t('nav.mainMenu', '메인 메뉴'), className: "side-nav__menu", children: [_jsxs("div", { className: "side-nav__menu-list", children: [(items || [])
|
|
51
62
|
.filter((it) => !it.section && (it.key || it.label))
|
|
52
|
-
.map((it, idx) => (_jsxs(React.Fragment, { children: [it.divider && _jsx("div", { className: "side-nav__separator" }), _jsx(SideNavButton, { item: it, active: it.key === activeKey, state: it.disabled ? 'disabled' : undefined, onClick: (_key, btnEl) => {
|
|
63
|
+
.map((it, idx) => (_jsxs(React.Fragment, { children: [it.divider && _jsx("div", { className: "side-nav__separator" }), _jsx(SideNavButton, { item: it, active: it.key === activeKey, state: it.disabled ? 'disabled' : undefined, onClick: (_key, btnEl, fromPointer) => {
|
|
53
64
|
if (it.disabled)
|
|
54
65
|
return;
|
|
55
66
|
if (it.href) {
|
|
56
67
|
window.open(it.href, '_blank', 'noopener,noreferrer');
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
if (fromPointer) {
|
|
69
|
+
try {
|
|
70
|
+
btnEl?.blur();
|
|
71
|
+
}
|
|
72
|
+
catch (err) { }
|
|
59
73
|
}
|
|
60
|
-
catch (err) { }
|
|
61
74
|
return;
|
|
62
75
|
}
|
|
63
76
|
onChange?.(it.key);
|
|
@@ -69,6 +82,10 @@ export default function SideNav(props) {
|
|
|
69
82
|
window.scrollTo({ top: 0 });
|
|
70
83
|
}
|
|
71
84
|
catch (_) { }
|
|
85
|
+
/* 키보드로 고르면 포커스가 레일 안에 남으므로 태블릿 레일도 펼친 채 둔다(:focus-within) —
|
|
86
|
+
아래 강제 접힘은 마우스가 들어와야 풀리므로 키보드 사용자에게 걸면 레일이 닫힌 채 포커스만 남는다. */
|
|
87
|
+
if (!fromPointer)
|
|
88
|
+
return;
|
|
72
89
|
try {
|
|
73
90
|
btnEl?.blur();
|
|
74
91
|
}
|
|
@@ -40,12 +40,28 @@ export type TableProps<T extends {
|
|
|
40
40
|
style?: CSSProperties;
|
|
41
41
|
}>;
|
|
42
42
|
onRowClick?: (item: T) => void;
|
|
43
|
+
/**
|
|
44
|
+
* 줄마다 누를 수 있는지. false 인 줄은 `onRowClick` 이 안 불리고, 버튼으로 읽히지도(role·Tab 정지점) 않으며,
|
|
45
|
+
* 손가락 커서·hover 강조도 없다. `onRowClick` 이 있을 때만 뜻이 있다. 미지정이면 모든 줄이 눌린다(기본).
|
|
46
|
+
*
|
|
47
|
+
* 예: 방문 기록 표에서 '안 옴' 줄은 열 것이 없다 — CSS 로 커서만 되돌리면 눌리기는 그대로이고 낭독기는 여전히 버튼이라 한다.
|
|
48
|
+
*/
|
|
49
|
+
isRowClickable?: (item: T) => boolean;
|
|
43
50
|
onRowMouseEnter?: (item: T, event: MouseEvent<HTMLDivElement>) => void;
|
|
44
51
|
onRowMouseLeave?: (item: T, event: MouseEvent<HTMLDivElement>) => void;
|
|
45
52
|
onRowDragStart?: (item: T, event: DragEvent<HTMLDivElement>) => void;
|
|
46
53
|
onRowDragEnd?: (item: T, event: DragEvent<HTMLDivElement>) => void;
|
|
47
54
|
draggingId?: string | number | null;
|
|
48
55
|
gridTemplateColumns?: string;
|
|
56
|
+
/**
|
|
57
|
+
* 좁은 화면(767px 이하)에서 쓸 열 틀. 주면 그 폭에서는 이 틀을 쓰고 **끌어 둔 폭(저장값)은 무시**한다 —
|
|
58
|
+
* 폭 조절 손잡이와 '칸 너비 초기화' 단추도 숨겨, 좁은 화면에서 끈 값이 넓은 화면의 저장값을 덮지 않게 한다.
|
|
59
|
+
* 넓은 화면으로 돌아오면 저장값이 그대로 돌아온다. 미지정이면 좁은 화면도 넓은 화면과 같다(기본).
|
|
60
|
+
*
|
|
61
|
+
* 열 틀은 인라인 CSS 변수로 들어가므로 앱 CSS 로는 `!important` 없이 못 이긴다 — 그래서 prop 이다.
|
|
62
|
+
* 숨길 칸은 `renderRow` 의 `className`·`fields[].className` 으로 앱이 가린다.
|
|
63
|
+
*/
|
|
64
|
+
mobileGridTemplateColumns?: string;
|
|
49
65
|
/** true면 헤더 경계선을 드래그해 컬럼 폭 조절 가능 */
|
|
50
66
|
resizable?: boolean;
|
|
51
67
|
/**
|
|
@@ -98,4 +114,4 @@ export type TableProps<T extends {
|
|
|
98
114
|
/** @public */
|
|
99
115
|
export default function Table<T extends {
|
|
100
116
|
id?: string | number;
|
|
101
|
-
}>({ fields, headerGroups, items, renderRow, onRowClick, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, resizable, resizeMode, resizeStorageKey, size, title, icon, count, collapsed, onToggleCollapse, headerActions, empty, loading, dragOver, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }: TableProps<T>): import("react").JSX.Element;
|
|
117
|
+
}>({ fields, headerGroups, items, renderRow, onRowClick, isRowClickable, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, mobileGridTemplateColumns, resizable, resizeMode, resizeStorageKey, size, title, icon, count, collapsed, onToggleCollapse, headerActions, empty, loading, dragOver, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }: TableProps<T>): import("react").JSX.Element;
|