@ihabdevteam/core 0.149.0 → 0.151.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 +60 -0
- package/dist/components.cjs.js +199 -186
- package/dist/components.esm.css +5 -5
- package/dist/hooks.cjs.js +5 -1
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core-tokens.css +1 -1
- package/dist/ihabdevteam-core.cjs.js +200 -187
- package/dist/ihabdevteam-core.css +1 -1
- package/dist/ihabdevteam-core.d.ts +407 -69
- package/dist/packages/core/src/components/Accordion.d.ts +6 -0
- package/dist/packages/core/src/components/Accordion.js +5 -2
- package/dist/packages/core/src/components/Alert.d.ts +7 -1
- package/dist/packages/core/src/components/Alert.js +12 -3
- package/dist/packages/core/src/components/BarChart.js +7 -2
- package/dist/packages/core/src/components/Board.d.ts +5 -2
- package/dist/packages/core/src/components/Board.js +9 -3
- package/dist/packages/core/src/components/BottomNav.js +5 -4
- package/dist/packages/core/src/components/Breadcrumb.css +9 -0
- package/dist/packages/core/src/components/Breadcrumb.d.ts +12 -1
- package/dist/packages/core/src/components/Breadcrumb.js +8 -2
- package/dist/packages/core/src/components/BulkBar.css +2 -0
- package/dist/packages/core/src/components/BulkBar.js +21 -2
- package/dist/packages/core/src/components/Button.d.ts +12 -2
- package/dist/packages/core/src/components/Button.js +70 -13
- package/dist/packages/core/src/components/Calendar.d.ts +6 -1
- package/dist/packages/core/src/components/Calendar.js +3 -3
- package/dist/packages/core/src/components/DatePicker.d.ts +7 -1
- package/dist/packages/core/src/components/DatePicker.js +25 -15
- package/dist/packages/core/src/components/Dropdown.d.ts +1 -0
- package/dist/packages/core/src/components/Dropdown.js +7 -5
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.d.ts +12 -1
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +25 -8
- package/dist/packages/core/src/components/EmptyState.d.ts +5 -1
- package/dist/packages/core/src/components/EmptyState.js +4 -1
- package/dist/packages/core/src/components/ErrorState.d.ts +5 -2
- package/dist/packages/core/src/components/ErrorState.js +3 -1
- package/dist/packages/core/src/components/FileUpload.d.ts +9 -1
- package/dist/packages/core/src/components/FileUpload.js +8 -2
- package/dist/packages/core/src/components/FilterSectionInfo.js +14 -2
- package/dist/packages/core/src/components/Header.js +8 -5
- package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -1
- package/dist/packages/core/src/components/Icon.d.ts +2 -1
- package/dist/packages/core/src/components/Icon.js +4 -1
- package/dist/packages/core/src/components/Loader.css +28 -0
- package/dist/packages/core/src/components/Loader.d.ts +5 -1
- package/dist/packages/core/src/components/Loader.js +4 -2
- package/dist/packages/core/src/components/NoticeBox.d.ts +1 -1
- package/dist/packages/core/src/components/NoticeBox.js +15 -6
- package/dist/packages/core/src/components/PanelRow.js +2 -1
- package/dist/packages/core/src/components/PassCard/PassCard.d.ts +8 -2
- package/dist/packages/core/src/components/PassCard/PassCard.js +25 -10
- package/dist/packages/core/src/components/PassCard/passCardProduct.d.ts +1 -1
- package/dist/packages/core/src/components/PassCard/passCardProduct.js +11 -2
- package/dist/packages/core/src/components/Popup/ConfirmPopup.d.ts +11 -2
- package/dist/packages/core/src/components/Popup/ConfirmPopup.js +18 -3
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +10 -8
- package/dist/packages/core/src/components/Popup/ListPopup.js +6 -6
- package/dist/packages/core/src/components/Popup/LoadTablePopup.css +7 -0
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +27 -5
- package/dist/packages/core/src/components/Popup/Popup.d.ts +8 -0
- package/dist/packages/core/src/components/Popup/Popup.js +6 -3
- package/dist/packages/core/src/components/Popup/SaveTablePopup.d.ts +5 -1
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.css +3 -0
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.d.ts +5 -0
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +10 -4
- package/dist/packages/core/src/components/Popup/WordDataUpdatePopup.css +9 -0
- package/dist/packages/core/src/components/ProgressIndicator.css +5 -0
- package/dist/packages/core/src/components/ProgressIndicator.d.ts +3 -1
- package/dist/packages/core/src/components/ProgressIndicator.js +5 -3
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.d.ts +4 -1
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +27 -15
- package/dist/packages/core/src/components/Rating.d.ts +8 -2
- package/dist/packages/core/src/components/Rating.js +22 -8
- package/dist/packages/core/src/components/Report/ReportDocument.d.ts +10 -7
- package/dist/packages/core/src/components/Report/ReportDocument.js +39 -13
- package/dist/packages/core/src/components/RingChart.d.ts +1 -1
- package/dist/packages/core/src/components/RingChart.js +1 -1
- package/dist/packages/core/src/components/SegmentedControl.d.ts +4 -0
- package/dist/packages/core/src/components/SegmentedControl.js +9 -1
- package/dist/packages/core/src/components/ShortcutBadge.css +16 -1
- package/dist/packages/core/src/components/ShortcutBadge.d.ts +1 -0
- package/dist/packages/core/src/components/SideNav.js +8 -2
- package/dist/packages/core/src/components/SummaryStrip.d.ts +1 -1
- package/dist/packages/core/src/components/SummaryStrip.js +2 -2
- package/dist/packages/core/src/components/Table.d.ts +1 -1
- package/dist/packages/core/src/components/Table.js +4 -1
- package/dist/packages/core/src/components/Tabs.d.ts +1 -0
- package/dist/packages/core/src/components/Tabs.js +4 -1
- package/dist/packages/core/src/components/TextField.css +5 -6
- package/dist/packages/core/src/components/TextField.js +46 -3
- package/dist/packages/core/src/components/Toast.js +16 -2
- package/dist/packages/core/src/components/Tooltip/Tooltip.css +3 -1
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +5 -2
- package/dist/packages/core/src/components/Tooltip/tooltipConstants.d.ts +10 -2
- package/dist/packages/core/src/components/Tooltip/tooltipConstants.js +10 -2
- package/dist/packages/core/src/components/TopBanner.d.ts +1 -1
- package/dist/packages/core/src/components/TopBanner.js +66 -8
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.d.ts +1 -0
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +65 -15
- package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +3 -2
- package/dist/packages/core/src/components/Training/NoisePlayerContext.js +10 -4
- package/dist/packages/core/src/components/Training/TrainingBlockButton.css +7 -0
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +3 -1
- package/dist/packages/core/src/components/Training/TrainingPagePopup.d.ts +18 -5
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +93 -28
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.d.ts +1 -0
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +16 -3
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +29 -11
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +19 -3
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +15 -1
- package/dist/packages/core/src/components/Training/TrainingWordCard.css +16 -0
- package/dist/packages/core/src/components/Training/TrainingWordCard.d.ts +6 -0
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +7 -2
- package/dist/packages/core/src/components/Training/trainingBlockButtonTypes.d.ts +2 -2
- package/dist/packages/core/src/components/Training/trainingMissStats.d.ts +10 -0
- package/dist/packages/core/src/components/Training/trainingMissStats.js +19 -1
- package/dist/packages/core/src/components/Training/useFloatingFocus.d.ts +13 -0
- package/dist/packages/core/src/components/Training/useFloatingFocus.js +59 -0
- package/dist/packages/core/src/components/Training/useNoiseFade.js +20 -11
- package/dist/packages/core/src/components/Training/useProgressTicker.d.ts +0 -18
- package/dist/packages/core/src/components/Training/useProgressTicker.js +59 -14
- package/dist/packages/core/src/components/Training/useTTSPlayback.d.ts +1 -1
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +56 -25
- package/dist/packages/core/src/components/Training/useWaveReady.d.ts +4 -1
- package/dist/packages/core/src/components/Training/useWaveReady.js +17 -8
- package/dist/packages/core/src/components/UpdateAlert.css +4 -2
- package/dist/packages/core/src/components/UpdateAlert.d.ts +3 -1
- package/dist/packages/core/src/components/UpdateAlert.js +21 -1
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.d.ts +2 -0
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +12 -4
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.d.ts +6 -0
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +26 -13
- package/dist/packages/core/src/components/Word/WordTableBlock.js +24 -21
- package/dist/packages/core/src/components/Word/WordTableThumbnail.js +1 -1
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -10
- package/dist/packages/core/src/components/Word/useWordTablePrint.d.ts +1 -1
- package/dist/packages/core/src/components/Word/useWordTablePrint.js +9 -3
- package/dist/packages/core/src/components/Word/wordScoreStats.d.ts +14 -1
- package/dist/packages/core/src/components/Word/wordScoreStats.js +7 -3
- package/dist/packages/core/src/components/Word/wordTablePrint.js +15 -3
- package/dist/packages/core/src/components/Word/wordTableReview.d.ts +58 -1
- package/dist/packages/core/src/components/Word/wordTableReview.js +75 -17
- package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +9 -0
- package/dist/packages/core/src/components/Word/wordTableSettings.js +11 -0
- package/dist/packages/core/src/components/WordTable.css +10 -0
- package/dist/packages/core/src/components/WordTable.js +39 -26
- package/dist/packages/core/src/components/barChartTypes.d.ts +2 -2
- package/dist/packages/core/src/components/buttonTypes.d.ts +8 -0
- package/dist/packages/core/src/components/updateAlertStack.d.ts +2 -0
- package/dist/packages/core/src/components/updateAlertStack.js +68 -0
- package/dist/packages/core/src/components/useCalendarMonth.d.ts +4 -1
- package/dist/packages/core/src/components/useCalendarMonth.js +6 -3
- package/dist/packages/core/src/components/useComboboxSuggestions.d.ts +7 -0
- package/dist/packages/core/src/components/useComboboxSuggestions.js +21 -2
- package/dist/packages/core/src/components/wordTableLayout.d.ts +4 -0
- package/dist/packages/core/src/components/wordTableLayout.js +6 -0
- package/dist/packages/core/src/components.d.ts +2 -1
- package/dist/packages/core/src/components.js +1 -1
- package/dist/packages/core/src/hooks/bodyScrollLock.js +8 -2
- package/dist/packages/core/src/hooks/focusReturn.d.ts +12 -0
- package/dist/packages/core/src/hooks/focusReturn.js +49 -0
- package/dist/packages/core/src/hooks/touchToggle.d.ts +22 -0
- package/dist/packages/core/src/hooks/touchToggle.js +114 -0
- package/dist/packages/core/src/hooks/useAnchoredMenu.d.ts +12 -3
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +37 -3
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.d.ts +4 -2
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +23 -11
- package/dist/packages/core/src/hooks/useIcon.js +29 -5
- package/dist/packages/core/src/hooks/useLucideIcon.d.ts +4 -0
- package/dist/packages/core/src/hooks/useLucideIcon.js +4 -0
- package/dist/packages/core/src/hooks/usePendingPress.d.ts +9 -0
- package/dist/packages/core/src/hooks/usePendingPress.js +26 -0
- package/dist/packages/core/src/hooks/usePrefersReducedMotion.d.ts +15 -0
- package/dist/packages/core/src/hooks/usePrefersReducedMotion.js +26 -0
- package/dist/packages/core/src/hooks/useRelativeDay.d.ts +2 -2
- package/dist/packages/core/src/hooks/useRelativeDay.js +12 -5
- package/dist/packages/core/src/hooks/useTablePaging.d.ts +27 -2
- package/dist/packages/core/src/hooks/useTablePaging.js +37 -5
- package/dist/packages/core/src/hooks/useWordAttributeFilter.d.ts +7 -0
- package/dist/packages/core/src/hooks/useWordAttributeFilter.js +25 -0
- package/dist/packages/core/src/i18n/index.js +9 -1
- package/dist/packages/core/src/i18n/locales/en.json +24 -2
- package/dist/packages/core/src/i18n/locales/ko.json +24 -2
- package/dist/packages/core/src/tokens.css +5 -0
- package/dist/packages/core/src/types/wordSearch.d.ts +12 -0
- package/dist/packages/core/src/utils/csv.d.ts +7 -2
- package/dist/packages/core/src/utils/csv.js +12 -5
- package/dist/packages/core/src/utils/dailySelectionUtils.js +44 -14
- package/dist/packages/core/src/utils/date.d.ts +8 -0
- package/dist/packages/core/src/utils/date.js +44 -10
- package/dist/packages/core/src/utils/format.d.ts +1 -1
- package/dist/packages/core/src/utils/format.js +6 -7
- package/dist/packages/core/src/utils/noiseSettingsStore.d.ts +7 -3
- package/dist/packages/core/src/utils/phonemeConvert.d.ts +9 -0
- package/dist/packages/core/src/utils/phonemeConvert.js +11 -0
- package/dist/packages/core/src/utils/phonemePairSearch.d.ts +7 -1
- package/dist/packages/core/src/utils/phonemePairSearch.js +45 -21
- package/dist/packages/core/src/utils/safeHref.d.ts +8 -0
- package/dist/packages/core/src/utils/safeHref.js +24 -0
- package/dist/packages/core/src/utils/savedWordTables.d.ts +19 -3
- package/dist/packages/core/src/utils/savedWordTables.js +27 -19
- package/dist/packages/core/src/utils/textSort.d.ts +9 -0
- package/dist/packages/core/src/utils/textSort.js +37 -1
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +31 -0
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +163 -0
- package/dist/packages/core/src/utils/wordContrast.d.ts +4 -2
- package/dist/packages/core/src/utils/wordContrast.js +8 -2
- package/dist/packages/core/src/utils/wordTrainingGenerators.d.ts +5 -2
- package/dist/packages/core/src/utils/wordTrainingGenerators.js +52 -2
- package/dist/packages/core/src/utils/wordTrainingPool.js +2 -2
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +15 -7
- package/dist/packages/core/src/utils/wordTrainingRuntimeTypes.d.ts +1 -1
- package/dist/packages/core/src/utils/wordTrainingRuntimeTypes.js +5 -4
- package/dist/packages/core/src/utils/wornDate.d.ts +12 -0
- package/dist/packages/core/src/utils/wornDate.js +15 -0
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +2 -2
- package/package.json +1 -1
|
@@ -13,7 +13,10 @@ export interface PromoBannerBgOption {
|
|
|
13
13
|
/** 이 배경 위에 얹을 제목/설명 텍스트가 밝은 색(light)이어야 하는지, 어두운 색(dark)이어야 하는지 */
|
|
14
14
|
textTone: 'light' | 'dark';
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* 배경 고르개 보기 목록. `label` 은 읽는 때의 i18n 언어를 따른다(`promoBannerBg.<key>`, ko 는 한국어 이름 그대로).
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
17
20
|
export declare const PROMO_BANNER_BG_OPTIONS: PromoBannerBgOption[];
|
|
18
21
|
import type { ComponentType } from 'react';
|
|
19
22
|
/** key → 실제 배경 컴포넌트. PromoBanner 등 사용처가 이걸로 렌더한다. 각 컴포넌트는 자신의
|
|
@@ -1,19 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
import i18n from '../../i18n';
|
|
2
|
+
/* 라벨은 **읽는 때의 언어**로 — 전에는 한국어가 박혀 영어 화면의 배경 고르개(어드민 공지 폼·놀이터)도 한국어였다.
|
|
3
|
+
배열·항목 모양(key·label·textTone)은 그대로 두고 label 만 읽을 때 사전(`promoBannerBg.<key>`)에서 꺼내는 getter 로 둔다
|
|
4
|
+
— 모듈을 읽은 순간의 언어로 굳지 않고, `{ ...o }` 로 펼치면 그때의 글자가 담긴다. ko 글자는 전과 같다. */
|
|
5
|
+
const 배경 = (key, 한국어, textTone) => ({
|
|
6
|
+
key,
|
|
7
|
+
get label() { return i18n.t(`promoBannerBg.${key}`, 한국어); },
|
|
8
|
+
textTone,
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* 배경 고르개 보기 목록. `label` 은 읽는 때의 i18n 언어를 따른다(`promoBannerBg.<key>`, ko 는 한국어 이름 그대로).
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
2
14
|
export const PROMO_BANNER_BG_OPTIONS = [
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
배경('main', '메인 그라디언트', 'light'),
|
|
16
|
+
배경('soft', '소프트', 'dark'),
|
|
17
|
+
배경('sub', '서브 컬러', 'light'),
|
|
18
|
+
배경('deep', '딥 그라디언트', 'light'),
|
|
19
|
+
배경('softSub', '소프트 서브', 'dark'),
|
|
20
|
+
배경('warm', '웜(강조)', 'dark'),
|
|
21
|
+
배경('dark', '다크', 'light'),
|
|
22
|
+
배경('radial', '방사형 그라디언트', 'light'),
|
|
23
|
+
배경('diagonal', '대각선 패턴', 'dark'),
|
|
24
|
+
배경('dots', '도트 패턴', 'dark'),
|
|
25
|
+
배경('outline', '아웃라인', 'dark'),
|
|
26
|
+
배경('success', '성공/완료', 'light'),
|
|
27
|
+
배경('naver', '네이버 블로그 안내', 'light'),
|
|
28
|
+
배경('instagram', '인스타그램 안내', 'light'),
|
|
17
29
|
];
|
|
18
30
|
import PromoBannerBgMain from './PromoBannerBgMain';
|
|
19
31
|
import PromoBannerBgSoft from './PromoBannerBgSoft';
|
|
@@ -3,9 +3,15 @@ import './Rating.css';
|
|
|
3
3
|
export type RatingSize = 'small' | 'medium' | 'large';
|
|
4
4
|
/** @public */
|
|
5
5
|
export interface RatingProps {
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* 현재 별점 (0 ~ max). 읽기 전용 모드에서는 소수점(예: 4.5)을 그대로 그린다.
|
|
8
|
+
* 고르는 모드에서는 **가장 가까운 정수로 반올림해** 그리고 키보드도 그 값에서 옮긴다(3.4 → 별 셋).
|
|
9
|
+
*/
|
|
7
10
|
value: number;
|
|
8
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* 값 변경 콜백. 생략 시 읽기 전용 모드로 동작.
|
|
13
|
+
* **값이 바뀔 때만** 부른다 — 지금 값과 같은 별을 다시 누르거나 끝에서 화살표를 눌러 머물면 부르지 않는다.
|
|
14
|
+
*/
|
|
9
15
|
onChange?: (value: number) => void;
|
|
10
16
|
/** 별 개수. 기본값 5 */
|
|
11
17
|
max?: number;
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useRef, useState } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import Icon from './Icon';
|
|
6
|
+
import { recentlyTouched } from '../hooks/touchToggle';
|
|
6
7
|
import './Rating.css';
|
|
7
8
|
const ICON_SIZE = {
|
|
8
9
|
small: 14,
|
|
@@ -14,16 +15,25 @@ export default function Rating({ value, onChange, max = 5, readOnly = false, siz
|
|
|
14
15
|
const { t } = useTranslation();
|
|
15
16
|
const isReadOnly = readOnly || !onChange;
|
|
16
17
|
const [hoverValue, setHoverValue] = useState(null);
|
|
17
|
-
|
|
18
|
+
/* 고르는 모드는 별 하나 단위라 **반올림한 값**으로 그리고 옮긴다 — 소수 value(3.4)를 그대로 두면 별 셋 반쯤이 칠해지는데
|
|
19
|
+
어느 라디오도 골라지지 않았고(aria-checked 없음), 화살표는 4.4·2.4 로 옮겨 가 정수 별과 어긋났다. 읽기 전용은 소수 그대로. */
|
|
20
|
+
const 값 = isReadOnly ? value : (Number.isFinite(value) ? Math.round(value) : 0);
|
|
21
|
+
const displayValue = hoverValue ?? 값;
|
|
18
22
|
const iconSize = ICON_SIZE[size];
|
|
23
|
+
/* 값이 바뀔 때만 onChange(2026-09-15 결정) — 소수 value 3.4 에서 별 3 을 누르면 3 으로 바뀌는 것이라 부른다 */
|
|
24
|
+
const 알림 = (다음) => { if (다음 !== value)
|
|
25
|
+
onChange?.(다음); };
|
|
19
26
|
const handleClick = (star) => {
|
|
20
27
|
if (isReadOnly)
|
|
21
28
|
return;
|
|
22
|
-
|
|
29
|
+
알림(star);
|
|
23
30
|
};
|
|
24
31
|
const handleMouseEnter = (star) => {
|
|
25
32
|
if (isReadOnly)
|
|
26
33
|
return;
|
|
34
|
+
// 톡 뒤 따라오는 호환 mouseenter 로는 미리보기를 켜지 않는다 — mouseleave 가 안 와 고른 값 대신 미리보기가 남았다
|
|
35
|
+
if (recentlyTouched())
|
|
36
|
+
return;
|
|
27
37
|
setHoverValue(star);
|
|
28
38
|
};
|
|
29
39
|
/* **라디오 무리는 Tab 자리 하나 + 화살표**가 ARIA 규칙이다. `role="radiogroup"` 만
|
|
@@ -34,7 +44,7 @@ export default function Rating({ value, onChange, max = 5, readOnly = false, siz
|
|
|
34
44
|
const 그룹 = useRef(null);
|
|
35
45
|
const 옮기기 = (다음) => {
|
|
36
46
|
const 잡은값 = Math.min(max, Math.max(1, 다음));
|
|
37
|
-
|
|
47
|
+
알림(잡은값);
|
|
38
48
|
const 칸 = Array.from(그룹.current?.querySelectorAll('[data-star]') ?? [])
|
|
39
49
|
.find((el) => el.dataset.star === String(잡은값));
|
|
40
50
|
칸?.focus();
|
|
@@ -45,11 +55,11 @@ export default function Rating({ value, onChange, max = 5, readOnly = false, siz
|
|
|
45
55
|
switch (e.key) {
|
|
46
56
|
case 'ArrowRight':
|
|
47
57
|
case 'ArrowUp':
|
|
48
|
-
옮기기((
|
|
58
|
+
옮기기((값 || 0) + 1);
|
|
49
59
|
break;
|
|
50
60
|
case 'ArrowLeft':
|
|
51
61
|
case 'ArrowDown':
|
|
52
|
-
옮기기((
|
|
62
|
+
옮기기((값 || 1) - 1);
|
|
53
63
|
break;
|
|
54
64
|
case 'Home':
|
|
55
65
|
옮기기(1);
|
|
@@ -62,7 +72,7 @@ export default function Rating({ value, onChange, max = 5, readOnly = false, siz
|
|
|
62
72
|
e.preventDefault();
|
|
63
73
|
};
|
|
64
74
|
/* Tab 자리를 받을 한 칸 — 고른 별, 없으면 첫 별 */
|
|
65
|
-
const 탭자리 =
|
|
75
|
+
const 탭자리 = 값 >= 1 && 값 <= max ? Math.round(값) : 1;
|
|
66
76
|
const handleMouseLeave = () => {
|
|
67
77
|
if (isReadOnly)
|
|
68
78
|
return;
|
|
@@ -75,7 +85,11 @@ export default function Rating({ value, onChange, max = 5, readOnly = false, siz
|
|
|
75
85
|
(별을 하나씩 읽히게 하면 "별·별·별…" 이 다섯 번 나올 뿐이라 묶음에 준다.) */
|
|
76
86
|
_jsx("div", { className: `rating rating--${size}${isReadOnly ? ' rating--readOnly' : ''} ${className}`.trim(), ref: 그룹, onKeyDown: 키, role: isReadOnly ? 'img' : 'radiogroup', "aria-label": isReadOnly
|
|
77
87
|
? t('rating.valueLabel', { value, max })
|
|
78
|
-
: (ariaLabel ?? t('rating.groupLabel', '별점')), onMouseLeave: handleMouseLeave,
|
|
88
|
+
: (ariaLabel ?? t('rating.groupLabel', '별점')), onMouseLeave: handleMouseLeave,
|
|
89
|
+
/* 터치를 떼거나 취소되면 미리보기를 푼다 */
|
|
90
|
+
onPointerUp: (e) => { if (e.pointerType === 'touch')
|
|
91
|
+
handleMouseLeave(); }, onPointerCancel: (e) => { if (e.pointerType === 'touch')
|
|
92
|
+
handleMouseLeave(); }, children: Array.from({ length: max }, (_, i) => {
|
|
79
93
|
const star = i + 1;
|
|
80
94
|
const fillRatio = Math.max(0, Math.min(1, displayValue - i));
|
|
81
95
|
const isHalf = fillRatio > 0 && fillRatio < 1;
|
|
@@ -85,6 +99,6 @@ export default function Rating({ value, onChange, max = 5, readOnly = false, siz
|
|
|
85
99
|
if (isReadOnly) {
|
|
86
100
|
return (_jsx("span", { className: "rating__item", "aria-hidden": "true", children: starNode }, star));
|
|
87
101
|
}
|
|
88
|
-
return (_jsx("button", { type: "button", className: "rating__item rating__item--interactive", role: "radio", "aria-checked":
|
|
102
|
+
return (_jsx("button", { type: "button", className: "rating__item rating__item--interactive", role: "radio", "aria-checked": 값 === star, "aria-label": t('rating.starLabel', { n: star }), "data-star": star, tabIndex: star === 탭자리 ? 0 : -1, onClick: () => handleClick(star), onMouseEnter: () => handleMouseEnter(star), children: starNode }, star));
|
|
89
103
|
}) }));
|
|
90
104
|
}
|
|
@@ -15,7 +15,8 @@ export interface ReportBlock {
|
|
|
15
15
|
/**
|
|
16
16
|
* 쪽을 나눌 때 한 덩어리로 다룰 묶음. 표 머리와 그 행들처럼 갈라지면 읽을 수
|
|
17
17
|
* 없는 것들에 같은 값을 준다 — 남은 자리가 모자라면 통째로 다음 장으로 간다.
|
|
18
|
-
* 묶음 하나가 한 장보다 크면 어쩔 수 없이 블록 단위로
|
|
18
|
+
* 묶음 하나가 한 장보다 크면 어쩔 수 없이 블록 단위로 쪼갠다 — 이때는 앞 장에 남은 자리부터 이어 채운다.
|
|
19
|
+
* `section` 이 다르면 같은 값이어도 한 묶음이 아니다.
|
|
19
20
|
*/
|
|
20
21
|
group?: string;
|
|
21
22
|
}
|
|
@@ -53,12 +54,14 @@ export interface ReportDocumentProps {
|
|
|
53
54
|
* 순서대로 채우다 넘치면 다음 장으로 넘긴다. 장마다 머리(제목 + 로고)와
|
|
54
55
|
* 바닥(로고·저작권 + 쪽 번호)이 반복된다.
|
|
55
56
|
*
|
|
56
|
-
* 재는
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
57
|
+
* 다시 재는 때는 블록들의 `id`·`section`·`group` 구성이나 장 규격(`pageWidth`·`pageHeight`·`margin`)이 바뀔 때,
|
|
58
|
+
* 처음 그릴 때(숨겨져 있다 보이게 될 때 포함), 그리고 **블록의 높이가 바뀔 때**다 — 재기 사본 블록마다
|
|
59
|
+
* ResizeObserver 를 걸어 `content` 가 바뀌어 높이가 달라지거나 글꼴·이미지가 늦게 와 커지면 다시 나눈다
|
|
60
|
+
* (여러 번 울려도 다음 틀(rAF)에 한 번). 렌더마다 재지는 않는다(재기 → 쪽 나눔 → 렌더가 끝없이 돈다).
|
|
61
|
+
* ResizeObserver 가 없는 환경에서는 높이만 바뀐 것은 못 알아채므로 `key` 를 바꿔 새로 붙인다.
|
|
62
|
+
*
|
|
63
|
+
* 쪽 나눔: 블록을 순서대로 채우다 넘치면 다음 장. `group` 이 남은 자리에 안 들어가면 통째로 다음 장으로 가는데,
|
|
64
|
+
* 묶음이 **한 장보다 크면** 어차피 쪼개야 하므로 앞 장에 남은 자리부터 이어 채운다.
|
|
62
65
|
*
|
|
63
66
|
* 화면에 그린 쪽 수는 다른 도구로 만든 PDF 와 다를 수 있다. 같은 내용을 그리는
|
|
64
67
|
* 높이가 서로 달라서인데, 쪽 수까지 맞춰야 하면 양쪽 본문 높이를 견줘야 한다.
|
|
@@ -14,12 +14,14 @@ const DEFAULT_PAGE_LABEL = (page, total) => `${page} / ${total}`;
|
|
|
14
14
|
* 순서대로 채우다 넘치면 다음 장으로 넘긴다. 장마다 머리(제목 + 로고)와
|
|
15
15
|
* 바닥(로고·저작권 + 쪽 번호)이 반복된다.
|
|
16
16
|
*
|
|
17
|
-
* 재는
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* 다시 재는 때는 블록들의 `id`·`section`·`group` 구성이나 장 규격(`pageWidth`·`pageHeight`·`margin`)이 바뀔 때,
|
|
18
|
+
* 처음 그릴 때(숨겨져 있다 보이게 될 때 포함), 그리고 **블록의 높이가 바뀔 때**다 — 재기 사본 블록마다
|
|
19
|
+
* ResizeObserver 를 걸어 `content` 가 바뀌어 높이가 달라지거나 글꼴·이미지가 늦게 와 커지면 다시 나눈다
|
|
20
|
+
* (여러 번 울려도 다음 틀(rAF)에 한 번). 렌더마다 재지는 않는다(재기 → 쪽 나눔 → 렌더가 끝없이 돈다).
|
|
21
|
+
* ResizeObserver 가 없는 환경에서는 높이만 바뀐 것은 못 알아채므로 `key` 를 바꿔 새로 붙인다.
|
|
22
|
+
*
|
|
23
|
+
* 쪽 나눔: 블록을 순서대로 채우다 넘치면 다음 장. `group` 이 남은 자리에 안 들어가면 통째로 다음 장으로 가는데,
|
|
24
|
+
* 묶음이 **한 장보다 크면** 어차피 쪼개야 하므로 앞 장에 남은 자리부터 이어 채운다.
|
|
23
25
|
*
|
|
24
26
|
* 화면에 그린 쪽 수는 다른 도구로 만든 PDF 와 다를 수 있다. 같은 내용을 그리는
|
|
25
27
|
* 높이가 서로 달라서인데, 쪽 수까지 맞춰야 하면 양쪽 본문 높이를 견줘야 한다.
|
|
@@ -88,6 +90,28 @@ export default function ReportDocument({ blocks, sectionTitles, logo, footer, pa
|
|
|
88
90
|
ro.observe(ruler);
|
|
89
91
|
return () => ro.disconnect();
|
|
90
92
|
}, []);
|
|
93
|
+
/* **내용 높이가 바뀌면 다시 잰다** — 짜임(id·section·group)이 같아도 메모가 길어지거나 글꼴·이미지가 늦게 와 블록 높이가 달라지면
|
|
94
|
+
예전 나눔 그대로 장이 넘쳤다(소리방향성은 id 에 메모 길이를 넣어 짜임을 바꾸는 우회를 썼다 — 그 길도 그대로 된다).
|
|
95
|
+
재기 사본 블록마다 ResizeObserver 를 걸고, 여러 번 울려도 다시 나눔은 rAF 한 번으로 모은다. 없는 환경에서는 지금처럼. */
|
|
96
|
+
const [다시재기, set다시재기] = useState(0);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const el = measureRef.current;
|
|
99
|
+
if (!el || typeof ResizeObserver === 'undefined')
|
|
100
|
+
return;
|
|
101
|
+
let 틀 = null;
|
|
102
|
+
const 미루기 = typeof requestAnimationFrame === 'function'
|
|
103
|
+
? (f) => requestAnimationFrame(f)
|
|
104
|
+
: (f) => setTimeout(f, 16);
|
|
105
|
+
const 거두기 = typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : (id) => clearTimeout(id);
|
|
106
|
+
const ro = new ResizeObserver(() => {
|
|
107
|
+
if (틀 != null)
|
|
108
|
+
return;
|
|
109
|
+
틀 = 미루기(() => { 틀 = null; set다시재기((n) => n + 1); });
|
|
110
|
+
});
|
|
111
|
+
Array.from(el.children).forEach((c) => ro.observe(c));
|
|
112
|
+
return () => { ro.disconnect(); if (틀 != null)
|
|
113
|
+
거두기(틀); };
|
|
114
|
+
}, [shape]);
|
|
91
115
|
useLayoutEffect(() => {
|
|
92
116
|
const el = measureRef.current;
|
|
93
117
|
const ruler = rulerRef.current;
|
|
@@ -134,12 +158,9 @@ export default function ReportDocument({ blocks, sectionTitles, logo, footer, pa
|
|
|
134
158
|
// 갈래가 바뀌면 새 장에서 시작한다.
|
|
135
159
|
if (cur.length && blocks[cur[0]].section !== blocks[grp[0]].section)
|
|
136
160
|
flush();
|
|
137
|
-
// 덩어리 전체가 남은 자리에 안 들어가면 통째로 다음 장으로.
|
|
138
|
-
// 소수 높이는 배율(transform: scale) 안에서 요소마다 1/64px 안팎 오차가 붙는다 — 딱 찬 결과지가 두 장이 되지 않게 조금 봐준다
|
|
139
|
-
if (cur.length && used + groupH > budget + 0.01)
|
|
140
|
-
flush();
|
|
141
161
|
if (groupH > budget) {
|
|
142
|
-
|
|
162
|
+
/* 덩어리 하나가 한 장보다 크면 쪼갤 수밖에 없다 — 그러니 **앞 장에 남은 자리부터** 이어 채운다. 전에는 먼저 새 장으로
|
|
163
|
+
넘긴 뒤 쪼개, 앞 장 아래가 비고 장이 하나 더 생겼다. */
|
|
143
164
|
grp.forEach((i) => {
|
|
144
165
|
if (cur.length && used + heights[i] > budget + 0.01)
|
|
145
166
|
flush();
|
|
@@ -148,16 +169,21 @@ export default function ReportDocument({ blocks, sectionTitles, logo, footer, pa
|
|
|
148
169
|
});
|
|
149
170
|
}
|
|
150
171
|
else {
|
|
172
|
+
// 덩어리 전체가 남은 자리에 안 들어가면 통째로 다음 장으로.
|
|
173
|
+
// 소수 높이는 배율(transform: scale) 안에서 요소마다 1/64px 안팎 오차가 붙는다 — 딱 찬 결과지가 두 장이 되지 않게 조금 봐준다
|
|
174
|
+
if (cur.length && used + groupH > budget + 0.01)
|
|
175
|
+
flush();
|
|
151
176
|
grp.forEach((i) => cur.push(i));
|
|
152
177
|
used += groupH;
|
|
153
178
|
}
|
|
154
179
|
});
|
|
155
180
|
flush();
|
|
156
|
-
|
|
181
|
+
/* 같은 나눔이면 상태를 그대로 둔다 — 다시 재기(ResizeObserver 첫 울림 등)마다 새 객체로 렌더가 한 번 더 도는 것을 막는다 */
|
|
182
|
+
setMeasured((앞) => (앞 && 앞.shape === shape && JSON.stringify(앞.pages) === JSON.stringify(packed) ? 앞 : { shape, pages: packed }));
|
|
157
183
|
// blocks 는 위 shape 로 대신 본다.
|
|
158
184
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
159
185
|
// 장 규격이 바뀌면 글이 다시 흘러 블록 높이가 달라진다 — 자(세로) 높이는 그대로라 자크기만으로는 다시 재지 않아 장이 넘쳤다
|
|
160
|
-
}, [shape, 자크기, pageWidth, pageHeight, margin]);
|
|
186
|
+
}, [shape, 자크기, pageWidth, pageHeight, margin, 다시재기]);
|
|
161
187
|
/* 번들러마다 SVG import 결과가 다르다 — Vite 는 문자열, Next 는 `{ src }` 객체다.
|
|
162
188
|
문자열만 보면 Next 앱에서 fallback 경로(`/logo/...`)로 떨어져 404 가 난다
|
|
163
189
|
(kscan 결과지의 바닥 로고가 실제로 깨졌다). 둘 다 받는다. */
|
|
@@ -35,7 +35,7 @@ export interface RingChartProps {
|
|
|
35
35
|
strokeWidth?: number;
|
|
36
36
|
/** 중앙 숫자 아래 라벨 (기본: "%") */
|
|
37
37
|
label?: string;
|
|
38
|
-
/** 중앙 텍스트(숫자+라벨) 전체 숨김 (기본: false) */
|
|
38
|
+
/** 중앙 텍스트(숫자+라벨) 전체 숨김 (기본: false — 단 `titleKey` 섹션의 `size="small"` 은 기본 숨김이고, `false` 를 명시하면 그린다) */
|
|
39
39
|
hideCenter?: boolean;
|
|
40
40
|
/** 중앙 숫자는 유지하고 아래 라벨("%")만 숨김 (기본: false) */
|
|
41
41
|
hideLabel?: boolean;
|
|
@@ -64,5 +64,5 @@ export default function RingChart({ titleKey, headRight, icon, showTotal, totalL
|
|
|
64
64
|
.filter(Boolean).join(' ') || undefined;
|
|
65
65
|
// StatChart(.manager-stat-card__icon)와 동일한 톤 — headRight 자리에 렌더(둘 중 하나만 사용)
|
|
66
66
|
const iconNode = icon && (_jsx("span", { className: "chart-section-icon", children: _jsx(Icon, { icon: icon, className: "chart-section-icon-inner" }) }));
|
|
67
|
-
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:
|
|
67
|
+
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: ringProps.hideCenter ?? isSmall }) })) }));
|
|
68
68
|
}
|
|
@@ -11,6 +11,10 @@ export type SegmentedOption = {
|
|
|
11
11
|
export interface SegmentedControlProps {
|
|
12
12
|
options?: SegmentedOption[];
|
|
13
13
|
value?: string;
|
|
14
|
+
/**
|
|
15
|
+
* 누른 칸의 key 로 불린다. **이미 고른 칸을 다시 눌러도 부른다**(화살표가 같은 칸에 머물 때도) — 다시 누름을
|
|
16
|
+
* "새로 고침" 으로 쓰는 자리가 있어 지금 동작을 지킨다. Dropdown·Rating·Tabs 는 값이 바뀔 때만 부른다.
|
|
17
|
+
*/
|
|
14
18
|
onChange?: (v: string) => void;
|
|
15
19
|
className?: string;
|
|
16
20
|
/** 버튼 높이 — Button과 동일한 숫자 size (32 / 40 / 48) */
|
|
@@ -54,8 +54,16 @@ export default function SegmentedControl(props) {
|
|
|
54
54
|
}
|
|
55
55
|
e.preventDefault();
|
|
56
56
|
};
|
|
57
|
+
const 이름 = (opt) => {
|
|
58
|
+
const 글자 = (v) => typeof v === 'string' || typeof v === 'number';
|
|
59
|
+
if (!글자(opt.label) || opt.count == null || opt.count === false || !글자(opt.count))
|
|
60
|
+
return undefined;
|
|
61
|
+
return `${opt.label}, ${opt.count}`;
|
|
62
|
+
};
|
|
57
63
|
/* Tab 자리를 받을 한 칸 — 고른 것이 있으면 그것, 없으면 고를 수 있는 첫 칸 */
|
|
58
64
|
const 탭자리 = 고를수있는.some((o) => o.key === value) ? value : 고를수있는[0]?.key;
|
|
59
|
-
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,
|
|
65
|
+
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,
|
|
66
|
+
/* 수를 이름에 넣는다 — 글자 이름표는 단추의 aria-label 이 되어 내용(수)이 이름 계산에서 빠졌다("전체" 로만 읽힘) */
|
|
67
|
+
ariaLabel: 이름(opt), disabled: opt.disabled, className: opt.key === value ? 'is-selected' : '', onClick: () => { if (!opt.disabled)
|
|
60
68
|
onChange(opt.key); }, size: size, shape: shape, useBorder: false, useBG: false }, opt.key))) }));
|
|
61
69
|
}
|
|
@@ -39,7 +39,22 @@
|
|
|
39
39
|
bottom: 0;
|
|
40
40
|
transform: translate(25%, 25%) scale(var(--shortcut-badge-scale, 1));
|
|
41
41
|
}
|
|
42
|
-
/* static — 흐름 안에 배치(
|
|
42
|
+
/* static — 흐름 안에 배치(position: static 기본). 배율은 transform 이 아니라 **크기**에 곱한다 — 흐름 안에서 scale() 을 걸면
|
|
43
|
+
그리는 모양만 커지고 차지하는 자리는 그대로라 옆 글자를 덮었다. 전에는 static 에 배율 규칙이 아예 없어 scale 이 안 먹었다. */
|
|
44
|
+
.shortcut-badge--static,
|
|
45
|
+
.shortcut-badge--static .shortcut-badge__group {
|
|
46
|
+
gap: calc(4px * var(--shortcut-badge-scale, 1));
|
|
47
|
+
}
|
|
48
|
+
.shortcut-badge--static .shortcut-badge__key {
|
|
49
|
+
min-width: calc(24px * var(--shortcut-badge-scale, 1));
|
|
50
|
+
min-height: calc(24px * var(--shortcut-badge-scale, 1));
|
|
51
|
+
padding: 0 calc(5px * var(--shortcut-badge-scale, 1));
|
|
52
|
+
border-width: calc(2px * var(--shortcut-badge-scale, 1));
|
|
53
|
+
font-size: calc(11px * var(--shortcut-badge-scale, 1));
|
|
54
|
+
}
|
|
55
|
+
.shortcut-badge--static .shortcut-badge__plus {
|
|
56
|
+
font-size: calc(11px * var(--shortcut-badge-scale, 1));
|
|
57
|
+
}
|
|
43
58
|
|
|
44
59
|
.shortcut-badge__group {
|
|
45
60
|
display: inline-flex;
|
|
@@ -19,6 +19,7 @@ export interface ShortcutBadgeProps {
|
|
|
19
19
|
* **`transform` 으로 키우지 말 것** — 배지가 모서리에 걸치는 `translate` 와 같은
|
|
20
20
|
* 속성이라 한쪽이 다른 쪽을 통째로 지운다. 이 prop 은 `--shortcut-badge-scale` 로
|
|
21
21
|
* 내려가고, 자리와 배율은 CSS 한 곳에서 합쳐 적힌다.
|
|
22
|
+
* `anchor="static"` 에서는 키캡 크기·글자에 곱해 흐름 안에서 차지하는 자리도 함께 커진다.
|
|
22
23
|
*/
|
|
23
24
|
scale?: number;
|
|
24
25
|
className?: string;
|
|
@@ -6,6 +6,7 @@ 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
|
+
import { isSafeOpenHref } from '../utils/safeHref';
|
|
9
10
|
/** 단추 이름에 개수를 붙인다 — 뱃지는 aria-hidden 이라 개수는 이름으로만 전해진다(BottomNav 와 같은 문구). */
|
|
10
11
|
function nameWithCount(t, label, count) {
|
|
11
12
|
return count && count > 0 ? t('nav.badgeLabel', { label, count, defaultValue: '{{label}}, {{count}}개' }) : label;
|
|
@@ -67,7 +68,9 @@ export default function SideNav(props) {
|
|
|
67
68
|
if (it.disabled)
|
|
68
69
|
return;
|
|
69
70
|
if (it.href) {
|
|
70
|
-
|
|
71
|
+
// http(s)·상대 경로·mailto·tel 만 연다 — javascript: 같은 주소가 그대로 열렸다
|
|
72
|
+
if (isSafeOpenHref(it.href))
|
|
73
|
+
window.open(it.href, '_blank', 'noopener,noreferrer');
|
|
71
74
|
if (fromPointer) {
|
|
72
75
|
try {
|
|
73
76
|
btnEl?.blur();
|
|
@@ -104,7 +107,10 @@ export default function SideNav(props) {
|
|
|
104
107
|
const headerEl = document.querySelector('.header');
|
|
105
108
|
if (headerEl) {
|
|
106
109
|
const supportsMatches = typeof headerEl.matches === 'function';
|
|
107
|
-
|
|
110
|
+
/* :hover 는 **호버가 되는 기기에서만** 믿는다 — iPad 는 톡한 요소에 :hover 가 붙어 남아, "마우스가 레일 위에 있다" 로
|
|
111
|
+
읽혀 강제 접힘을 건너뛰었다(메뉴를 골라도 레일이 펼친 채 본문을 가림) */
|
|
112
|
+
const 호버기기 = window.matchMedia('(hover: hover)').matches;
|
|
113
|
+
const isHovered = 호버기기 && supportsMatches ? headerEl.matches(':hover') : false;
|
|
108
114
|
if (!isHovered) {
|
|
109
115
|
headerEl.classList.add('header--force-collapsed');
|
|
110
116
|
const onEnter = () => {
|
|
@@ -38,7 +38,7 @@ export interface SummaryStripProps {
|
|
|
38
38
|
/**
|
|
39
39
|
* 화면 위쪽에 놓이는 한 줄 요약 — "지금 다루고 있는 대상"을 이름표와 값으로 늘어놓는다.
|
|
40
40
|
*
|
|
41
|
-
* 값 길이가 제각각이라 균등 그리드에 앉힌다. 긴 값은 칸을 밀어내지 않고
|
|
41
|
+
* 값 길이가 제각각이라 균등 그리드에 앉힌다. 긴 값은 칸을 밀어내지 않고 말줄임된다 — 글자·수 값이면 `title` 로 전체 값을 단다.
|
|
42
42
|
*
|
|
43
43
|
* 값의 서식은 부르는 쪽이 정해 넘긴다 — 빈 값을 `-` 로 적을지, 날짜를 어떻게 쓸지,
|
|
44
44
|
* 여러 값을 한 칸에 묶을지(`오른손 / NC / 연구실 내`)는 화면마다 다르다.
|
|
@@ -4,7 +4,7 @@ import './SummaryStrip.css';
|
|
|
4
4
|
/**
|
|
5
5
|
* 화면 위쪽에 놓이는 한 줄 요약 — "지금 다루고 있는 대상"을 이름표와 값으로 늘어놓는다.
|
|
6
6
|
*
|
|
7
|
-
* 값 길이가 제각각이라 균등 그리드에 앉힌다. 긴 값은 칸을 밀어내지 않고
|
|
7
|
+
* 값 길이가 제각각이라 균등 그리드에 앉힌다. 긴 값은 칸을 밀어내지 않고 말줄임된다 — 글자·수 값이면 `title` 로 전체 값을 단다.
|
|
8
8
|
*
|
|
9
9
|
* 값의 서식은 부르는 쪽이 정해 넘긴다 — 빈 값을 `-` 로 적을지, 날짜를 어떻게 쓸지,
|
|
10
10
|
* 여러 값을 한 칸에 묶을지(`오른손 / NC / 연구실 내`)는 화면마다 다르다.
|
|
@@ -30,5 +30,5 @@ import './SummaryStrip.css';
|
|
|
30
30
|
*/
|
|
31
31
|
export default function SummaryStrip({ items, actions, columns, flat = false, className, style, }) {
|
|
32
32
|
const cols = columns ?? items.length;
|
|
33
|
-
return (_jsx("section", { className: cx('summary-strip', flat && 'summary-strip--flat', className), style: { ['--summary-strip-cols']: String(cols), ...style }, children: _jsxs("div", { className: "summary-strip__row", children: [_jsx("div", { className: "summary-strip__items", children: items.map((item, i) => (_jsxs("div", { className: "summary-strip__item", children: [_jsx("p", { className: "summary-strip__label caption-1", children: item.label }), _jsx("p", { className: "summary-strip__value body-3b", children: item.value })] }, i))) }), actions && _jsx("div", { className: "summary-strip__actions", children: actions })] }) }));
|
|
33
|
+
return (_jsx("section", { className: cx('summary-strip', flat && 'summary-strip--flat', className), style: { ['--summary-strip-cols']: String(cols), ...style }, children: _jsxs("div", { className: "summary-strip__row", children: [_jsx("div", { className: "summary-strip__items", children: items.map((item, i) => (_jsxs("div", { className: "summary-strip__item", children: [_jsx("p", { className: "summary-strip__label caption-1", children: item.label }), _jsx("p", { className: "summary-strip__value body-3b", title: typeof item.value === 'string' || typeof item.value === 'number' ? String(item.value) : undefined, children: item.value })] }, i))) }), actions && _jsx("div", { className: "summary-strip__actions", children: actions })] }) }));
|
|
34
34
|
}
|
|
@@ -108,7 +108,7 @@ export type TableProps<T extends {
|
|
|
108
108
|
onDrop?: (e: DragEvent<HTMLDivElement>) => void;
|
|
109
109
|
/** 펼쳐진 행에 렌더할 콘텐츠. expandedId와 함께 사용 */
|
|
110
110
|
renderExpandedRow?: (item: T) => ReactNode;
|
|
111
|
-
/** 현재 펼쳐진 행의 id */
|
|
111
|
+
/** 현재 펼쳐진 행의 id(여럿이면 Set). 글자로 견준다 — '5' 와 5 는 같은 줄. */
|
|
112
112
|
expandedId?: string | number | Set<string | number> | null;
|
|
113
113
|
};
|
|
114
114
|
/** @public */
|
|
@@ -326,7 +326,10 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
|
|
|
326
326
|
? (e) => startResize(i, e) : undefined, children: f.label }, i))) }), 폭조절 && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: "chevrons-left-right-ellipsis", ariaLabel: t('table.resetColumns', '칸 너비 초기화'), showTooltipInfo: t('table.resetColumns', '칸 너비 초기화'), className: "table-fields__reset", onClick: 자동맞춤 }))] }), _jsx("div", { className: "table-body", children: loading ? (_jsx(TableRow, { variant: "loading" })) : items.length === 0 && empty ? (_jsx(TableRow, { variant: "empty", children: empty })) : items.map((item, idx) => {
|
|
327
327
|
const isDragging = draggingId != null && item.id === draggingId;
|
|
328
328
|
const 눌림 = !!onRowClick && (isRowClickable ? isRowClickable(item) : true);
|
|
329
|
-
|
|
329
|
+
/* 글자로 견준다 — URL·폼에서 온 '5' 와 자료의 5 가 같은 줄인데 `===` 로는 안 펴졌다 */
|
|
330
|
+
const isExpanded = expandedId != null && item.id != null && (expandedId instanceof Set
|
|
331
|
+
? expandedId.has(item.id) || [...expandedId].some(x => String(x) === String(item.id))
|
|
332
|
+
: String(item.id) === String(expandedId));
|
|
330
333
|
return (_jsx(TableRow
|
|
331
334
|
// 타입까지 열쇠에 — id 없는 줄(idx 0)과 id:0, 1 과 '1' 이 같은 열쇠가 되어 목록을 바꾼 뒤 옛 줄이 남았다
|
|
332
335
|
, { className: `${눌림 ? ' table-row--clickable' : ''}${onRowDragStart ? ' table-row--draggable' : ''}${isDragging ? ' table-row--dragging' : ''}`.trim(), draggable: !!onRowDragStart,
|
|
@@ -11,6 +11,7 @@ export type TabItem = {
|
|
|
11
11
|
export interface TabsProps {
|
|
12
12
|
items?: TabItem[];
|
|
13
13
|
value?: string;
|
|
14
|
+
/** 고른 탭의 key 로 불린다. **값이 바뀔 때만** — 지금 탭을 다시 누르거나 화살표가 지금 탭에 머물면 부르지 않는다. */
|
|
14
15
|
onChange?: (key: string) => void;
|
|
15
16
|
className?: string;
|
|
16
17
|
/** 탭 버튼 높이 — SegmentedControl과 동일 체계 (32 / 40 / 48) */
|
|
@@ -63,5 +63,8 @@ export default function Tabs({ items = [], value, onChange, className = '', size
|
|
|
63
63
|
focusTabAt(tabs.length - 1, tabs);
|
|
64
64
|
};
|
|
65
65
|
const activeItem = items.find(item => item.key === activeKey);
|
|
66
|
-
return (_jsxs("div", { className: `tabs ${className}`.trim(), children: [_jsx("div", { ref: listRef, className: `tabs__list tabs__list--${size}`, role: "tablist", "aria-label": t('tabs.ariaLabel', '탭'), children: items.map(item => (_jsx("button", { id: `tabs__tab-${uid}-${item.key}`, type: "button", role: "tab", "aria-selected": item.key === activeKey, "aria-controls": `tabs__panel-${uid}-${item.key}`, disabled: item.disabled, tabIndex: item.key === 탭자리키 ? 0 : -1, className: `tabs__tab body-2${item.key === activeKey ? ' tabs__tab--active' : ''}`,
|
|
66
|
+
return (_jsxs("div", { className: `tabs ${className}`.trim(), children: [_jsx("div", { ref: listRef, className: `tabs__list tabs__list--${size}`, role: "tablist", "aria-label": t('tabs.ariaLabel', '탭'), children: items.map(item => (_jsx("button", { id: `tabs__tab-${uid}-${item.key}`, type: "button", role: "tab", "aria-selected": item.key === activeKey, "aria-controls": `tabs__panel-${uid}-${item.key}`, disabled: item.disabled, tabIndex: item.key === 탭자리키 ? 0 : -1, className: `tabs__tab body-2${item.key === activeKey ? ' tabs__tab--active' : ''}`,
|
|
67
|
+
/* 값이 바뀔 때만 onChange(2026-09-15 결정) — 지금 탭을 다시 누르면 아무 일도 없다 */
|
|
68
|
+
onClick: () => { if (item.key !== value)
|
|
69
|
+
onChange?.(item.key); }, onKeyDown: handleKeyDown, children: item.label }, item.key))) }), activeItem && (_jsx("div", { id: `tabs__panel-${uid}-${activeItem.key}`, role: "tabpanel", "aria-labelledby": `tabs__tab-${uid}-${activeItem.key}`, tabIndex: 0, className: "tabs__panel", children: activeItem.panel }, activeItem.key))] }));
|
|
67
70
|
}
|
|
@@ -193,13 +193,12 @@
|
|
|
193
193
|
margin-right: 10px;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
/* 자동완성 드롭다운 (suggestions)
|
|
196
|
+
/* 자동완성 드롭다운 (suggestions) — body 로 포털되어 칸 좌표에 fixed 로 뜬다(TextField.tsx 가 left·top·width·z-index 를 준다).
|
|
197
|
+
팝업 본문의 overflow 에 잘리지 않게 칸 곁이 아니라 body 끝에 둔다. */
|
|
197
198
|
.text-field__suggestions {
|
|
198
|
-
position:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
top: 100%;
|
|
202
|
-
margin-top: var(--spacing-xs);
|
|
199
|
+
position: fixed;
|
|
200
|
+
box-sizing: border-box;
|
|
201
|
+
margin: 0;
|
|
203
202
|
background: var(--color-white);
|
|
204
203
|
border: 1px solid var(--color-whiteLine);
|
|
205
204
|
box-shadow: var(--shadow-dropdown);
|
|
@@ -4,7 +4,9 @@ import { cx } from '../utils/cx';
|
|
|
4
4
|
import { useId, useEffect, useLayoutEffect, useRef, useCallback, useState } from 'react';
|
|
5
5
|
import { devWarnOnce } from '../utils/devWarn';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { createPortal } from 'react-dom';
|
|
7
8
|
import Icon from './Icon';
|
|
9
|
+
import { anchoredMenuZIndex } from '../hooks/useAnchoredMenu';
|
|
8
10
|
import Button from './Button';
|
|
9
11
|
import { useComboboxSuggestions } from './useComboboxSuggestions';
|
|
10
12
|
import './TextField.css';
|
|
@@ -43,6 +45,44 @@ export default function TextField(props) {
|
|
|
43
45
|
});
|
|
44
46
|
/* 후보 목록이 지금 DOM 에 있는가 — 아래 렌더 조건과 **같은 식**이어야 한다 */
|
|
45
47
|
const 목록떴나 = !!suggestions && combobox.open && combobox.filtered.length > 0;
|
|
48
|
+
/* 후보 목록은 **body 로 빼서** 칸 좌표에 fixed 로 띄운다 — 칸 아래 absolute 로 두었더니 팝업 본문(overflow: auto)에
|
|
49
|
+
잘려 창 아래쪽 칸(훈련 묶음의 그룹 이름)에서 후보가 보이지 않았다. Dropdown 메뉴와 같은 방식(층·data-tooltip-ignore). */
|
|
50
|
+
const rootRef = useRef(null);
|
|
51
|
+
const [listStyle, setListStyle] = useState(null);
|
|
52
|
+
const 목록자리재기 = useCallback(() => {
|
|
53
|
+
const root = rootRef.current;
|
|
54
|
+
if (!root || typeof window === 'undefined')
|
|
55
|
+
return;
|
|
56
|
+
const r = root.getBoundingClientRect();
|
|
57
|
+
const 최대높이 = 220 + 8 + 2; // max-height + padding + 테두리
|
|
58
|
+
const 아래 = window.innerHeight - r.bottom;
|
|
59
|
+
const 위로 = 아래 < 최대높이 + 8 && r.top > 최대높이 + 8;
|
|
60
|
+
setListStyle({
|
|
61
|
+
position: 'fixed',
|
|
62
|
+
left: r.left,
|
|
63
|
+
width: r.width,
|
|
64
|
+
...(위로 ? { bottom: window.innerHeight - r.top + 8 } : { top: r.bottom + 8 }),
|
|
65
|
+
zIndex: anchoredMenuZIndex(root),
|
|
66
|
+
});
|
|
67
|
+
}, []);
|
|
68
|
+
useIsoLayoutEffect(() => {
|
|
69
|
+
if (!목록떴나)
|
|
70
|
+
return;
|
|
71
|
+
목록자리재기();
|
|
72
|
+
/* 굴리거나 창 크기가 바뀌면 칸을 따라간다 — 목록 안을 굴리는 것은 무시 */
|
|
73
|
+
const 굴림 = (e) => {
|
|
74
|
+
const t = e.target;
|
|
75
|
+
if (t && t.nodeType === 1 && t.closest?.('.text-field__suggestions'))
|
|
76
|
+
return;
|
|
77
|
+
목록자리재기();
|
|
78
|
+
};
|
|
79
|
+
window.addEventListener('scroll', 굴림, true);
|
|
80
|
+
window.addEventListener('resize', 목록자리재기);
|
|
81
|
+
return () => {
|
|
82
|
+
window.removeEventListener('scroll', 굴림, true);
|
|
83
|
+
window.removeEventListener('resize', 목록자리재기);
|
|
84
|
+
};
|
|
85
|
+
}, [목록떴나, 목록자리재기]);
|
|
46
86
|
// 외부 inputRef + 내부 autoRef 동시 연결 — 외부에서 전달받은 ref 객체(.current)에 노드를 채우는
|
|
47
87
|
// React의 표준 ref 병합/전달(forwarding) 패턴이며, 이 외에 구현할 방법이 없다
|
|
48
88
|
/* eslint-disable react-hooks/immutability */
|
|
@@ -146,6 +186,9 @@ export default function TextField(props) {
|
|
|
146
186
|
});
|
|
147
187
|
}
|
|
148
188
|
},
|
|
189
|
+
/* 조합 중 중간값('삭')에서 후보 목록이 깜빡이지 않게 — 훅이 조합 중에는 직전 목록을 둔다 */
|
|
190
|
+
onCompositionStart: suggestions ? combobox.handleCompositionStart : undefined,
|
|
191
|
+
onCompositionEnd: suggestions ? combobox.handleCompositionEnd : undefined,
|
|
149
192
|
onFocus: ((e) => {
|
|
150
193
|
onFocus?.(e);
|
|
151
194
|
if (suggestions)
|
|
@@ -189,7 +232,7 @@ export default function TextField(props) {
|
|
|
189
232
|
// 값이 있으면(:placeholder-shown 해제) CSS로 숨긴다.
|
|
190
233
|
const showPlaceholderOverlay = variant === 'display' && !newline && !multiline && !!placeholder;
|
|
191
234
|
const renderInputArea = () => (_jsxs(_Fragment, { children: [renderInput(), showPlaceholderOverlay && (_jsx("span", { className: "text-field__placeholder", "aria-hidden": "true", children: placeholder }))] }));
|
|
192
|
-
return (_jsxs("div", { className: cls, children: [label && (_jsxs("label", { htmlFor: inputId, className: "text-field__label caption-1", children: [label, required && _jsx("span", { className: "text-field__required", children: "*" })] })), _jsxs("div", { className: "text-field__wrap", children: [leftIcon && (_jsx("span", { className: "text-field__icon text-field__icon--left", children: _jsx(Icon, { icon: leftIcon, label: "", className: "", svgClassName: "text-field__icon-svg", size: iconSize }) })), renderInputArea(), maxLength != null && showCount && (_jsxs("span", { className: `text-field__count caption-2${isOver ? ' text-field__count--over' : ''}`, children: [len, "/", maxLength] })), showClear && (_jsx(Button, { size: 24, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, className: "text-field__clear", ariaLabel: t('textField.clearAriaLabel', '입력값 지우기'),
|
|
235
|
+
return (_jsxs("div", { ref: rootRef, className: cls, children: [label && (_jsxs("label", { htmlFor: inputId, className: "text-field__label caption-1", children: [label, required && _jsx("span", { className: "text-field__required", children: "*" })] })), _jsxs("div", { className: "text-field__wrap", children: [leftIcon && (_jsx("span", { className: "text-field__icon text-field__icon--left", children: _jsx(Icon, { icon: leftIcon, label: "", className: "", svgClassName: "text-field__icon-svg", size: iconSize }) })), renderInputArea(), maxLength != null && showCount && (_jsxs("span", { className: `text-field__count caption-2${isOver ? ' text-field__count--over' : ''}`, children: [len, "/", maxLength] })), showClear && (_jsx(Button, { size: 24, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, className: "text-field__clear", ariaLabel: t('textField.clearAriaLabel', '입력값 지우기'),
|
|
193
236
|
// 폼 안에서 쓰일 때 기본값(submit)으로 눌려 폼이 제출되지 않도록.
|
|
194
237
|
htmlType: "button",
|
|
195
238
|
// mousedown 은 포커스가 입력에서 떠나는 것만 막는다(preventDefault 는 click 을 막지 않는다).
|
|
@@ -204,10 +247,10 @@ export default function TextField(props) {
|
|
|
204
247
|
setInnerLen(0);
|
|
205
248
|
}
|
|
206
249
|
onChange?.('');
|
|
207
|
-
}, tabIndex: -1 })), rightIcon && !showClear && (_jsx("span", { className: "text-field__icon text-field__icon--right", children: _jsx(Icon, { icon: rightIcon, label: "", className: "", svgClassName: "text-field__icon-svg", size: iconSize }) }))] }),
|
|
250
|
+
}, tabIndex: -1 })), rightIcon && !showClear && (_jsx("span", { className: "text-field__icon text-field__icon--right", children: _jsx(Icon, { icon: rightIcon, label: "", className: "", svgClassName: "text-field__icon-svg", size: iconSize }) }))] }), 목록떴나 && (typeof document === 'undefined' ? null : createPortal(_jsx("ul", { id: combobox.listId, className: "text-field__suggestions", role: "listbox", style: listStyle ?? { position: 'fixed', visibility: 'hidden' }, "data-tooltip-ignore": true, children: combobox.filtered.map((s, i) => (_jsx("li", { id: `${combobox.listId}-option-${i}`, role: "option", "aria-selected": i === combobox.activeIdx, className: `text-field__suggestion body-2${i === combobox.activeIdx ? ' is-active' : ''}`,
|
|
208
251
|
// 선택은 click 에서 한다. onMouseDown 만 두면 마우스로는 눌리지만 스크린리더가
|
|
209
252
|
// 이 option 을 활성화할 때는 click 만 발생해 아무 일도 일어나지 않는다.
|
|
210
253
|
// mousedown 에서는 기본 동작(포커스 이동 → 입력 blur)을 막아, click 이 도달하기
|
|
211
254
|
// 전에 목록이 닫히지 않게 한다 — blur 쪽 120ms 딜레이에 기대지 않아도 된다.
|
|
212
|
-
onMouseDown: e => e.preventDefault(), onClick: () => { combobox.handleSelect(s); autoRef.current?.focus(); }, children: s }, `${s}-${i}`))) })), (helperText || error) && (_jsx("div", { className: "text-field__footer", children: _jsx("span", { id: helperId, className: "text-field__helper caption-1", role: error ? 'alert' : undefined, children: error || helperText }) }))] }));
|
|
255
|
+
onMouseDown: e => e.preventDefault(), onClick: () => { combobox.handleSelect(s); autoRef.current?.focus(); }, children: s }, `${s}-${i}`))) }), document.body)), (helperText || error) && (_jsx("div", { className: "text-field__footer", children: _jsx("span", { id: helperId, className: "text-field__helper caption-1", role: error ? 'alert' : undefined, children: error || helperText }) }))] }));
|
|
213
256
|
}
|