@ihabdevteam/core 0.321.0 → 0.323.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 +617 -0
- package/dist/components.cjs.js +183 -183
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +3 -3
- package/dist/i18n.cjs.js +3 -3
- package/dist/ihabdevteam-core.cjs.js +187 -187
- package/dist/ihabdevteam-core.d.ts +108 -66
- package/dist/packages/core/src/components/Accordion.js +3 -3
- package/dist/packages/core/src/components/Alert.js +2 -2
- package/dist/packages/core/src/components/BarChart.js +120 -119
- package/dist/packages/core/src/components/BottomNav.js +18 -18
- package/dist/packages/core/src/components/BulkBar.js +25 -25
- package/dist/packages/core/src/components/Button.js +16 -16
- package/dist/packages/core/src/components/Calendar.js +17 -17
- package/dist/packages/core/src/components/ChartSection.js +30 -30
- package/dist/packages/core/src/components/CountTile.js +38 -38
- package/dist/packages/core/src/components/DatePicker.js +59 -59
- package/dist/packages/core/src/components/Dialog.js +14 -14
- package/dist/packages/core/src/components/Effect/EffectPartyPopperHost.js +8 -8
- package/dist/packages/core/src/components/FilterPanel.js +6 -6
- package/dist/packages/core/src/components/FilterSectionInfo.js +6 -6
- package/dist/packages/core/src/components/GameIntro.js +1 -1
- package/dist/packages/core/src/components/HearingDeviceInfo.d.ts +5 -5
- package/dist/packages/core/src/components/HearingDeviceInfo.js +2 -2
- package/dist/packages/core/src/components/Icon.js +2 -2
- package/dist/packages/core/src/components/ItemActions.js +16 -16
- package/dist/packages/core/src/components/LandingHeader.js +6 -6
- package/dist/packages/core/src/components/LineChart.js +24 -24
- package/dist/packages/core/src/components/Loader.js +5 -5
- package/dist/packages/core/src/components/NoticeBox.js +3 -3
- package/dist/packages/core/src/components/PageTitle.js +2 -2
- package/dist/packages/core/src/components/Pagination.js +4 -4
- package/dist/packages/core/src/components/Panel.js +2 -2
- package/dist/packages/core/src/components/PanelRow.d.ts +2 -4
- package/dist/packages/core/src/components/PanelRow.js +8 -15
- package/dist/packages/core/src/components/PassCard/PassCard.js +9 -9
- package/dist/packages/core/src/components/PassCard/passCardProduct.js +2 -2
- package/dist/packages/core/src/components/PaymentPlan.js +5 -5
- package/dist/packages/core/src/components/Popup/ConfirmPopup.js +29 -29
- package/dist/packages/core/src/components/Popup/CouponInputPopup.js +2 -2
- package/dist/packages/core/src/components/Popup/DeviceListPopup.d.ts +4 -8
- package/dist/packages/core/src/components/Popup/DeviceListPopup.js +1 -1
- package/dist/packages/core/src/components/Popup/DeviceSideTable.js +19 -26
- package/dist/packages/core/src/components/Popup/ListPopup.js +6 -6
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +10 -10
- package/dist/packages/core/src/components/Popup/Popup.js +27 -27
- package/dist/packages/core/src/components/Popup/SaveTablePopup.js +5 -5
- package/dist/packages/core/src/components/Popup/TrainingResultPopup.js +4 -4
- package/dist/packages/core/src/components/Popup/customerDetailUtils.js +18 -18
- package/dist/packages/core/src/components/Popup/useDiscardGuard.js +4 -4
- package/dist/packages/core/src/components/ProgressIndicator.js +5 -5
- package/dist/packages/core/src/components/PromoBannerBg/promoBannerBg.js +16 -16
- package/dist/packages/core/src/components/Rating.js +20 -20
- package/dist/packages/core/src/components/Report/ReportDocument.js +21 -21
- package/dist/packages/core/src/components/RingChart.js +5 -5
- package/dist/packages/core/src/components/SectionHeadFloat.js +15 -15
- package/dist/packages/core/src/components/SegmentedControl.js +34 -34
- package/dist/packages/core/src/components/SideNav.js +37 -37
- package/dist/packages/core/src/components/Slider.js +2 -2
- package/dist/packages/core/src/components/SoundCompass.js +5 -5
- package/dist/packages/core/src/components/Table.js +257 -98
- package/dist/packages/core/src/components/Tabs.js +12 -12
- package/dist/packages/core/src/components/TextField.js +34 -34
- package/dist/packages/core/src/components/Toast.js +6 -6
- package/dist/packages/core/src/components/Tooltip/MenuTooltip.js +4 -4
- package/dist/packages/core/src/components/Tooltip/Tooltip.js +22 -22
- package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +4 -4
- package/dist/packages/core/src/components/TopBanner.js +28 -28
- package/dist/packages/core/src/components/Training/AudioPlayerWaveForm.js +11 -11
- package/dist/packages/core/src/components/Training/CategoryIcon.js +2 -2
- package/dist/packages/core/src/components/Training/NoisePlayerContext.d.ts +7 -4
- package/dist/packages/core/src/components/Training/NoisePlayerContext.js +12 -12
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingBlock.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingBlockButton.js +10 -10
- package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +21 -21
- package/dist/packages/core/src/components/Training/TrainingCard.css +13 -1
- package/dist/packages/core/src/components/Training/TrainingCard.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingCardGroup.js +6 -6
- package/dist/packages/core/src/components/Training/TrainingPagePopup.js +83 -83
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +128 -128
- package/dist/packages/core/src/components/Training/TrainingStepCard.js +18 -18
- package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +12 -12
- package/dist/packages/core/src/components/Training/TrainingStepTextBox.js +13 -13
- package/dist/packages/core/src/components/Training/TrainingStepTitle.js +4 -4
- package/dist/packages/core/src/components/Training/TrainingWordCard.js +17 -17
- package/dist/packages/core/src/components/Training/WordTablePopup.js +5 -5
- package/dist/packages/core/src/components/Training/audioPlaybackRules.js +8 -8
- package/dist/packages/core/src/components/Training/audioTimeline.js +25 -25
- package/dist/packages/core/src/components/Training/oneShotSound.d.ts +1 -1
- package/dist/packages/core/src/components/Training/oneShotSound.js +14 -14
- package/dist/packages/core/src/components/Training/stepAnswerable.d.ts +2 -2
- package/dist/packages/core/src/components/Training/trainingMissStats.js +22 -22
- package/dist/packages/core/src/components/Training/trainingPopupUtils.js +7 -7
- package/dist/packages/core/src/components/Training/useAudioPreview.js +15 -15
- package/dist/packages/core/src/components/Training/useFloatingFocus.js +14 -14
- package/dist/packages/core/src/components/Training/useNoiseFade.js +22 -22
- package/dist/packages/core/src/components/Training/useObjectUrls.js +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.d.ts +2 -2
- package/dist/packages/core/src/components/Training/usePlaybackSequencer.js +23 -23
- package/dist/packages/core/src/components/Training/useProgressTicker.js +33 -33
- package/dist/packages/core/src/components/Training/useTTSPlayback.js +17 -17
- package/dist/packages/core/src/components/Training/useWaveReady.js +11 -11
- package/dist/packages/core/src/components/UpdateAlert.js +6 -6
- package/dist/packages/core/src/components/Word/PhonemeSelectBlock.js +7 -1
- package/dist/packages/core/src/components/Word/TableFilterPanel.js +3 -3
- package/dist/packages/core/src/components/Word/WordSearchResultBlock.js +18 -18
- package/dist/packages/core/src/components/Word/WordTableBlock.js +28 -28
- package/dist/packages/core/src/components/Word/WordTableButton.css +16 -2
- package/dist/packages/core/src/components/Word/WordTableThumbnail.css +6 -1
- package/dist/packages/core/src/components/Word/generatedDraft.js +14 -14
- package/dist/packages/core/src/components/Word/useWordTableDisplaySettings.js +7 -7
- package/dist/packages/core/src/components/Word/wordScoreStats.js +17 -17
- package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
- package/dist/packages/core/src/components/Word/wordTableRecommend.js +13 -13
- package/dist/packages/core/src/components/Word/wordTableReview.js +28 -28
- package/dist/packages/core/src/components/Word/wordTableSettings.d.ts +8 -11
- package/dist/packages/core/src/components/Word/wordTableSettings.js +48 -36
- package/dist/packages/core/src/components/WordTable.js +130 -130
- package/dist/packages/core/src/components/barChartData.d.ts +16 -1
- package/dist/packages/core/src/components/barChartData.js +49 -29
- package/dist/packages/core/src/components/barChartTypes.js +8 -8
- package/dist/packages/core/src/components/tableTrackList.js +11 -11
- package/dist/packages/core/src/components/updateAlertStack.js +28 -28
- package/dist/packages/core/src/components/useBarChartControl.js +4 -6
- package/dist/packages/core/src/components/useComboboxSuggestions.js +17 -17
- package/dist/packages/core/src/components/useToastLifecycle.js +18 -18
- package/dist/packages/core/src/components/wordTableLayout.d.ts +22 -7
- package/dist/packages/core/src/components/wordTableLayout.js +5 -5
- package/dist/packages/core/src/context/I18nProvider.js +6 -6
- package/dist/packages/core/src/hooks/focusReturn.js +27 -27
- package/dist/packages/core/src/hooks/inputModality.d.ts +1 -1
- package/dist/packages/core/src/hooks/inputModality.js +14 -14
- package/dist/packages/core/src/hooks/touchToggle.js +39 -39
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +30 -30
- package/dist/packages/core/src/hooks/useDailyTrainingSelection.js +28 -28
- package/dist/packages/core/src/hooks/useDeviceManagement.d.ts +5 -17
- package/dist/packages/core/src/hooks/useDeviceManagement.js +20 -8
- package/dist/packages/core/src/hooks/useDocumentTitle.js +10 -10
- package/dist/packages/core/src/hooks/useEditableList.js +4 -4
- package/dist/packages/core/src/hooks/useFocusTrap.js +2 -2
- package/dist/packages/core/src/hooks/useFontScale.js +6 -6
- package/dist/packages/core/src/hooks/useIcon.js +7 -7
- package/dist/packages/core/src/hooks/useListControls.js +2 -2
- package/dist/packages/core/src/hooks/usePageResetOnChange.d.ts +3 -3
- package/dist/packages/core/src/hooks/usePageResetOnChange.js +6 -6
- package/dist/packages/core/src/hooks/usePasswordChange.js +4 -4
- package/dist/packages/core/src/hooks/usePendingPress.js +7 -7
- package/dist/packages/core/src/hooks/useRelativeDay.js +4 -4
- package/dist/packages/core/src/hooks/useSoundCompassExam.js +6 -6
- package/dist/packages/core/src/hooks/useTablePaging.d.ts +7 -7
- package/dist/packages/core/src/hooks/useTablePaging.js +40 -34
- package/dist/packages/core/src/hooks/useTrainingSessionLog.js +35 -35
- package/dist/packages/core/src/hooks/useVisitSchedule.js +4 -4
- package/dist/packages/core/src/hooks/useWordAttributeFilter.js +14 -14
- package/dist/packages/core/src/hooks/useWordMeanings.js +5 -5
- package/dist/packages/core/src/hooks/useWordTrainingFilter.d.ts +3 -1
- package/dist/packages/core/src/hooks/useWordTrainingFilter.js +14 -12
- package/dist/packages/core/src/i18n/locales/en.json +13 -2
- package/dist/packages/core/src/i18n/locales/ko.json +12 -1
- package/dist/packages/core/src/types/wordSearch.d.ts +9 -0
- package/dist/packages/core/src/utils/audioMeter.js +5 -5
- package/dist/packages/core/src/utils/audioPeaks.d.ts +1 -1
- package/dist/packages/core/src/utils/audioPeaks.js +18 -18
- package/dist/packages/core/src/utils/blockGrid.js +4 -4
- package/dist/packages/core/src/utils/chunkSkew.js +9 -9
- package/dist/packages/core/src/utils/clipboard.js +3 -3
- package/dist/packages/core/src/utils/dailySelectionUtils.js +12 -12
- package/dist/packages/core/src/utils/date.js +12 -12
- package/dist/packages/core/src/utils/devWarn.d.ts +2 -2
- package/dist/packages/core/src/utils/devWarn.js +7 -7
- package/dist/packages/core/src/utils/deviceKind.d.ts +37 -7
- package/dist/packages/core/src/utils/deviceKind.js +53 -10
- package/dist/packages/core/src/utils/errorMessage.js +7 -7
- package/dist/packages/core/src/utils/filterPanelSummary.d.ts +1 -1
- package/dist/packages/core/src/utils/filterPanelSummary.js +16 -16
- package/dist/packages/core/src/utils/format.js +4 -4
- package/dist/packages/core/src/utils/getTrainingData.js +9 -9
- package/dist/packages/core/src/utils/imageConfig.js +17 -17
- package/dist/packages/core/src/utils/koreanSyllable.js +18 -18
- package/dist/packages/core/src/utils/navScroll.d.ts +2 -2
- package/dist/packages/core/src/utils/navScroll.js +6 -6
- package/dist/packages/core/src/utils/phonemeConvert.d.ts +7 -0
- package/dist/packages/core/src/utils/phonemeConvert.js +19 -10
- package/dist/packages/core/src/utils/phonemeGroups.js +2 -2
- package/dist/packages/core/src/utils/phonemePairSearch.d.ts +1 -0
- package/dist/packages/core/src/utils/phonemePairSearch.js +118 -102
- package/dist/packages/core/src/utils/phonemeSectionCodes.d.ts +52 -0
- package/dist/packages/core/src/utils/phonemeSectionCodes.js +76 -0
- package/dist/packages/core/src/utils/prefetchChunk.js +6 -6
- package/dist/packages/core/src/utils/pressable.d.ts +1 -1
- package/dist/packages/core/src/utils/pressable.js +3 -3
- package/dist/packages/core/src/utils/safeHref.js +4 -4
- package/dist/packages/core/src/utils/savedWordTables.js +5 -5
- package/dist/packages/core/src/utils/sessionTab.d.ts +7 -7
- package/dist/packages/core/src/utils/sessionTab.js +9 -9
- package/dist/packages/core/src/utils/speak.js +12 -12
- package/dist/packages/core/src/utils/textSort.d.ts +5 -5
- package/dist/packages/core/src/utils/textSort.js +47 -47
- package/dist/packages/core/src/utils/theme.js +3 -3
- package/dist/packages/core/src/utils/trainingItemResults.js +3 -3
- package/dist/packages/core/src/utils/ttsConfig.js +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.d.ts +2 -2
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +48 -48
- package/dist/packages/core/src/utils/wordContrast.js +3 -3
- package/dist/packages/core/src/utils/wordPairIndex.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingGenerators.js +4 -4
- package/dist/packages/core/src/utils/wordTrainingPool.js +15 -15
- package/dist/packages/core/src/utils/wordTrainingRuntime.js +6 -6
- package/dist/packages/core/src/utils/wornDate.d.ts +3 -3
- package/dist/packages/core/src/utils/wornDate.js +14 -14
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +4 -4
- package/package.json +1 -1
|
@@ -19,21 +19,24 @@ export declare const NOISE_RATIO_STEP = 0.1;
|
|
|
19
19
|
export declare function noiseRatioOf(settings: Pick<CoreNoiseSettings, 'level' | 'mainVolume'> & {
|
|
20
20
|
noiseRatio?: number;
|
|
21
21
|
}): number;
|
|
22
|
-
|
|
22
|
+
/** `effectiveSpeechVolume`·`effectiveNoiseVolume`·`noiseElementVolume` 이 받는 것 —
|
|
23
|
+
* **공개 `d.ts` 에 이름이 그대로 나가는 자리**라 영어로 둔다. 안쪽 이름은 한글로 쓰는 저장소지만,
|
|
24
|
+
* 소비자가 편집기에서 보는 이름은 영어라야 한다(사용자 지시 2026-10-13). */
|
|
25
|
+
type SpeechVolumeSettings = Pick<CoreNoiseSettings, 'level' | 'mainVolume'> & {
|
|
23
26
|
noiseRatio?: number;
|
|
24
27
|
};
|
|
25
28
|
/** @public 문제 소리 볼륨(0~1) = mainVolume. 비율과 상관없다. */
|
|
26
|
-
export declare function effectiveSpeechVolume(settings:
|
|
29
|
+
export declare function effectiveSpeechVolume(settings: SpeechVolumeSettings): number;
|
|
27
30
|
/**
|
|
28
31
|
* @public 소음이 **들리는 크기**(0~2, 원본 소음 파일 기준) = mainVolume × 비율. 켜짐 여부는 보지
|
|
29
32
|
* 않는다. 결과 기록·파형이 쓴다. 소음 `<audio>` 에 거는 값은 `noiseElementVolume`.
|
|
30
33
|
*/
|
|
31
|
-
export declare function effectiveNoiseVolume(settings:
|
|
34
|
+
export declare function effectiveNoiseVolume(settings: SpeechVolumeSettings): number;
|
|
32
35
|
/**
|
|
33
36
|
* @public 소음 `<audio>` 볼륨(0~1) = min(1, 들리는 크기 ÷ gain). gain 은 소음 파일이 원본보다 몇 배
|
|
34
37
|
* 큰지 — 기본은 `configureStorage({ noiseFileGain })`(없으면 1).
|
|
35
38
|
*/
|
|
36
|
-
export declare function noiseElementVolume(settings:
|
|
39
|
+
export declare function noiseElementVolume(settings: SpeechVolumeSettings, gain?: number): number;
|
|
37
40
|
/**
|
|
38
41
|
* @public 설정에 조각을 덮는다 — `{ ...base, ...patch }` 에 비율 규칙 하나를 더한 것.
|
|
39
42
|
*
|
|
@@ -26,11 +26,11 @@ export const pctToLevel = (pct) => (pct / 100) * LEVEL_MAX;
|
|
|
26
26
|
export const NOISE_RATIO_MAX = 2;
|
|
27
27
|
/** @public 소음 비율 칸(배). */
|
|
28
28
|
export const NOISE_RATIO_STEP = 0.1;
|
|
29
|
-
const
|
|
29
|
+
const clampRatio = (r) => Math.min(NOISE_RATIO_MAX, Math.max(0, r));
|
|
30
30
|
/** 옛 기록에서 옮긴 비율의 범위 — 말귀 noiseRatioOfPct 와 같다(0.1~2). */
|
|
31
|
-
const
|
|
31
|
+
const LEGACY_RATIO_MIN = 0.1;
|
|
32
32
|
/* 부동소수 찌꺼기만 턴다(0.30000000000000004 → 0.3) — 0.1 칸으로 반올림하지는 않는다 */
|
|
33
|
-
const
|
|
33
|
+
const roundOff = (r) => Math.round(r * 1e6) / 1e6;
|
|
34
34
|
/**
|
|
35
35
|
* @public 설정의 소음 비율(배). `noiseRatio` 가 있으면 그것(0~2 로 자름), 없으면 **옛 값에서
|
|
36
36
|
* 들리던 그대로** 옮긴다 — (level/9) ÷ mainVolume 을 **반올림 없이** 0.1~2 로 조인다(말귀 `noiseRatioOfPct` 와 같은 범위).
|
|
@@ -40,26 +40,26 @@ const 찌꺼기털기 = (r) => Math.round(r * 1e6) / 1e6;
|
|
|
40
40
|
export function noiseRatioOf(settings) {
|
|
41
41
|
const { noiseRatio } = settings;
|
|
42
42
|
if (typeof noiseRatio === 'number' && Number.isFinite(noiseRatio))
|
|
43
|
-
return
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
if (!(
|
|
43
|
+
return clampRatio(noiseRatio);
|
|
44
|
+
const noise = Math.min(LEVEL_MAX, Math.max(0, Number(settings.level) || 0)) / LEVEL_MAX;
|
|
45
|
+
const main = Number(settings.mainVolume);
|
|
46
|
+
if (!(main > 0))
|
|
47
47
|
return 1;
|
|
48
|
-
if (!(
|
|
48
|
+
if (!(noise > 0))
|
|
49
49
|
return 0;
|
|
50
|
-
return
|
|
50
|
+
return roundOff(Math.min(NOISE_RATIO_MAX, Math.max(LEGACY_RATIO_MIN, noise / main)));
|
|
51
51
|
}
|
|
52
|
-
const
|
|
52
|
+
const mainVolumeOf = (settings) => Math.min(1, Math.max(0, Number(settings.mainVolume) || 0));
|
|
53
53
|
/** @public 문제 소리 볼륨(0~1) = mainVolume. 비율과 상관없다. */
|
|
54
54
|
export function effectiveSpeechVolume(settings) {
|
|
55
|
-
return
|
|
55
|
+
return mainVolumeOf(settings);
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* @public 소음이 **들리는 크기**(0~2, 원본 소음 파일 기준) = mainVolume × 비율. 켜짐 여부는 보지
|
|
59
59
|
* 않는다. 결과 기록·파형이 쓴다. 소음 `<audio>` 에 거는 값은 `noiseElementVolume`.
|
|
60
60
|
*/
|
|
61
61
|
export function effectiveNoiseVolume(settings) {
|
|
62
|
-
return
|
|
62
|
+
return mainVolumeOf(settings) * noiseRatioOf(settings);
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* @public 소음 `<audio>` 볼륨(0~1) = min(1, 들리는 크기 ÷ gain). gain 은 소음 파일이 원본보다 몇 배
|
|
@@ -76,9 +76,9 @@ export default function NoiseSettingsTooltip({ anchorRect, onClose, hideNoiseRow
|
|
|
76
76
|
catch (e) { }
|
|
77
77
|
}, [noisePlayer]);
|
|
78
78
|
/* 소음 종류 보기 — **한 곳**이다. 아래 참 줄과 폭 그림자 줄이 같은 배열을 쓴다 */
|
|
79
|
-
const
|
|
79
|
+
const kindOptions = [
|
|
80
80
|
{ key: NONE_KEY, label: t('training.noise.type.none', '없음') },
|
|
81
81
|
...getNoiseTypeOptions(t).map(opt => ({ key: opt.value, label: opt.label })),
|
|
82
82
|
];
|
|
83
|
-
return (_jsx(Tooltip, { anchorRect: anchorRect, offset: 12, onClose: onClose, ariaLabel: t('training.trainingSettingsTitle', '훈련 설정'), isMobilePopup: false, closeOnOutsideClick: false, header: _jsxs("div", { className: "noise-settings__header", children: [_jsx("span", { children: t('training.trainingSettingsTitle', '훈련 설정') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, ariaLabel: t('common.close', '닫기'), onClick: onClose })] }), children: _jsxs(Panel, { className: "noise-settings", useBorder: false, labelWidth: 72, children: [_jsx(SliderPanelRow, { title: t('training.playVolumeTitle', '전체 소리'), valueLabel: `${volPct}%`, value: volPct, min: 0, max: 100, step: 10, size: sliderSize, onChange: handlePlayVolumeChange }), _jsx(SliderPanelRow, { title: t('training.playSpeedTitle', '재생 속도'), valueLabel: `${playbackRate.toFixed(2)}x`, value: playbackRate, min: PLAY_SPEED_MIN, max: PLAY_SPEED_MAX, step: PLAY_SPEED_STEP, size: sliderSize, onChange: handlePlaySpeedChange }), !hideNoiseRows && (_jsxs(_Fragment, { children: [_jsx(PanelRow, { variant: "custom", label: t('training.noiseTypeTitle', '소음 종류'), useFullWidth: true, children: _jsx(SegmentedControl, { ariaLabel: t('training.noiseTypeTitle', '소음 종류'), size: 40, shape: "rounded", options:
|
|
83
|
+
return (_jsx(Tooltip, { anchorRect: anchorRect, offset: 12, onClose: onClose, ariaLabel: t('training.trainingSettingsTitle', '훈련 설정'), isMobilePopup: false, closeOnOutsideClick: false, header: _jsxs("div", { className: "noise-settings__header", children: [_jsx("span", { children: t('training.trainingSettingsTitle', '훈련 설정') }), _jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: "x", variant: "transparent-gray", useBorder: false, ariaLabel: t('common.close', '닫기'), onClick: onClose })] }), children: _jsxs(Panel, { className: "noise-settings", useBorder: false, labelWidth: 72, children: [_jsx(SliderPanelRow, { title: t('training.playVolumeTitle', '전체 소리'), valueLabel: `${volPct}%`, value: volPct, min: 0, max: 100, step: 10, size: sliderSize, onChange: handlePlayVolumeChange }), _jsx(SliderPanelRow, { title: t('training.playSpeedTitle', '재생 속도'), valueLabel: `${playbackRate.toFixed(2)}x`, value: playbackRate, min: PLAY_SPEED_MIN, max: PLAY_SPEED_MAX, step: PLAY_SPEED_STEP, size: sliderSize, onChange: handlePlaySpeedChange }), !hideNoiseRows && (_jsxs(_Fragment, { children: [_jsx(PanelRow, { variant: "custom", label: t('training.noiseTypeTitle', '소음 종류'), useFullWidth: true, children: _jsx(SegmentedControl, { ariaLabel: t('training.noiseTypeTitle', '소음 종류'), size: 40, shape: "rounded", options: kindOptions, value: noiseEnabled ? noiseType : NONE_KEY, onChange: handleTypeChange }) }), _jsx(SliderPanelRow, { title: t('training.noiseRatioTitle', '소음 비율'), valueLabel: t('training.noiseRatioValue', '{{ratio}}배', { ratio: ratio.toFixed(1) }), value: ratio, min: 0, max: NOISE_RATIO_MAX, step: NOISE_RATIO_STEP, size: sliderSize, disabled: !noiseEnabled, onChange: handleRatioChange })] })), hideNoiseRows && (_jsx("div", { className: "noise-settings__width-ghost", "aria-hidden": "true", inert: true, children: _jsx(PanelRow, { variant: "custom", label: '\u00a0', useFullWidth: true, children: _jsx(SegmentedControl, { ariaLabel: "width-ghost", size: 40, shape: "rounded", options: kindOptions, value: NONE_KEY, onChange: () => { } }) }) }))] }) }));
|
|
84
84
|
}
|
|
@@ -7,7 +7,7 @@ import Section from '../Section';
|
|
|
7
7
|
import './TrainingBlock.css';
|
|
8
8
|
import TrainingBlockButton from './TrainingBlockButton';
|
|
9
9
|
import { useWindowEvent } from '../../hooks/useWindowEvent';
|
|
10
|
-
import {
|
|
10
|
+
import { wasKeyboardPress } from '../../hooks/inputModality';
|
|
11
11
|
const BTN_COUNT_MAX = 20;
|
|
12
12
|
/** @public */
|
|
13
13
|
export default function TrainingBlock(props) {
|
|
@@ -31,24 +31,24 @@ export default function TrainingBlock(props) {
|
|
|
31
31
|
/* eslint-enable react-hooks/set-state-in-effect */
|
|
32
32
|
useWindowEvent('speechear.data.updated', () => setDataTick((v) => v + 1));
|
|
33
33
|
/* 마지막 '더보기' 는 누르면 사라진다 — 초점이 거기 있었으면 새로 붙은 첫 항목으로 옮긴다(안 옮기면 body 로 떨어졌다) */
|
|
34
|
-
const
|
|
35
|
-
const
|
|
34
|
+
const listRef = useRef(null);
|
|
35
|
+
const moreFocusRef = useRef(null);
|
|
36
36
|
function loadMore() {
|
|
37
|
-
const
|
|
38
|
-
|
|
37
|
+
const button = document.activeElement;
|
|
38
|
+
moreFocusRef.current = wasKeyboardPress() && button && button.closest?.('.training-block__more') ? visible : null;
|
|
39
39
|
setVisible((v) => Math.min(total, v + BTN_COUNT_MAX));
|
|
40
40
|
}
|
|
41
41
|
useEffect(() => {
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
42
|
+
const firstNew = moreFocusRef.current;
|
|
43
|
+
if (firstNew == null)
|
|
44
44
|
return;
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
45
|
+
moreFocusRef.current = null;
|
|
46
|
+
const active = document.activeElement;
|
|
47
|
+
if (active && active !== document.body)
|
|
48
48
|
return;
|
|
49
|
-
|
|
49
|
+
listRef.current?.querySelectorAll('.training-block-button')[firstNew]?.focus();
|
|
50
50
|
}, [visible]);
|
|
51
|
-
return (_jsxs(Section, { type: sectionType, title: type?.label, icon: type?.icon, disc: type?.desc, className: "training-block", children: [_jsx("div", { className: "training-list", ref:
|
|
51
|
+
return (_jsxs(Section, { type: sectionType, title: type?.label, icon: type?.icon, disc: type?.desc, className: "training-block", children: [_jsx("div", { className: "training-list", ref: listRef, children: (itemsAll || []).slice(0, visible).map((item, idx) => {
|
|
52
52
|
const itemForBtn = item;
|
|
53
53
|
const depthForBtn = depthLabel;
|
|
54
54
|
return (_jsx(TrainingBlockButton, { index: idx, type: type, trainingData: itemForBtn, depth: depthForBtn, renderPopup: renderPopup, completedIds: completedIds, selectedIds: selectedIds, size: size, useDescriptionTooltip: useDescriptionTooltip, onRemove: typeof onRemove === 'function' && selectedIds != null && itemForBtn?.id != null && selectedIds.has(String(itemForBtn.id))
|
|
@@ -179,18 +179,18 @@ export default function TrainingBlockButton(props) {
|
|
|
179
179
|
const isCompact = size === 'compact';
|
|
180
180
|
const isSmall = size === 'small';
|
|
181
181
|
// 유형 이름은 지금 언어로 — 등록된 원본의 한국어 라벨을 써 영어 화면 계획 바구니에 '소음 하 문장 듣기' 가 떴다
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const trainingTypeName =
|
|
185
|
-
? (
|
|
182
|
+
const typeSource = isSmall && type?.key ? getTrainingTypes().find(x => x.key === type.key) : undefined;
|
|
183
|
+
const typeI18nKey = typeSource ? TRAINING_TYPE_I18N_KEY[typeSource.key] : undefined;
|
|
184
|
+
const trainingTypeName = typeSource
|
|
185
|
+
? (typeI18nKey ? t(`trainingType.${typeI18nKey}.title`, { defaultValue: typeSource.label }) : (typeSource.label ?? ''))
|
|
186
186
|
: '';
|
|
187
187
|
// 타입 라벨이 실제로 렌더되는지(타이틀과 같으면 숨김) — 레이아웃 여백 정리에 사용.
|
|
188
|
-
const
|
|
188
|
+
const typeText = (!!showTypeLabel && !!type?.label && type.label !== String(itemTitle)) ? String(type.label)
|
|
189
189
|
: (isSmall && !!trainingTypeName && trainingTypeName !== String(itemTitle)) ? trainingTypeName
|
|
190
190
|
: '';
|
|
191
|
-
const typeLabelRendered =
|
|
191
|
+
const typeLabelRendered = !!typeText;
|
|
192
192
|
/* 그려지는 수 — 아래 본문과 **같은 값을 본다.** 전에는 JSX 안에서만 셈해서 이름이 못 썼다. */
|
|
193
|
-
const
|
|
193
|
+
const shownCount = countLabelOverride !== undefined ? countLabelOverride : countLabel;
|
|
194
194
|
/**
|
|
195
195
|
* **눈에 보이는 줄 그대로 이름을 짓는다**(말귀 요청 (65) 의 뒷자락, 2026-10-02).
|
|
196
196
|
*
|
|
@@ -209,11 +209,11 @@ export default function TrainingBlockButton(props) {
|
|
|
209
209
|
* 때만 넣는다. 그래야 이름과 눈이 어긋나지 않는다(WCAG 2.5.3, 보이는 이름이 읽히는 이름
|
|
210
210
|
* 안에 들어 있어야 한다). 부르는 쪽이 `ariaLabel` 을 주면 그쪽이 먼저다.
|
|
211
211
|
*/
|
|
212
|
-
const
|
|
212
|
+
const ariaParts = [typeText, typeof itemTitle === 'string' ? itemTitle : '', shownCount]
|
|
213
213
|
.map(x => String(x ?? '').trim()).filter(Boolean).join(' ');
|
|
214
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: btnRef, className: `training-block-button${isList ? ' training-block-button--list' : ''}${isRow ? ' training-block-button--row' : ''}${isCompact ? ' training-block-button--compact' : ''}${isSmall ? ' training-block-button--small' : ''}${isCompleted ? ' training-block-button--completed' : ''}${isSelected ? ' training-block-button--selected' : ''}${typeLabelRendered && !isSmall ? ' training-block-button--withType' : ''}${isSmall && !typeLabelRendered ? ' training-block-button--no-type-label' : ''}`, onClick: handleActivate, onPointerEnter: handleBtnHoverOn, onPointerLeave: handleBtnHoverClose, role: "button", tabIndex: 0, onKeyDown: handleKeyDown, "aria-label": typeof ariaLabel === 'string' ? ariaLabel : (
|
|
214
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: btnRef, className: `training-block-button${isList ? ' training-block-button--list' : ''}${isRow ? ' training-block-button--row' : ''}${isCompact ? ' training-block-button--compact' : ''}${isSmall ? ' training-block-button--small' : ''}${isCompleted ? ' training-block-button--completed' : ''}${isSelected ? ' training-block-button--selected' : ''}${typeLabelRendered && !isSmall ? ' training-block-button--withType' : ''}${isSmall && !typeLabelRendered ? ' training-block-button--no-type-label' : ''}`, onClick: handleActivate, onPointerEnter: handleBtnHoverOn, onPointerLeave: handleBtnHoverClose, role: "button", tabIndex: 0, onKeyDown: handleKeyDown, "aria-label": typeof ariaLabel === 'string' ? ariaLabel : (ariaParts || undefined),
|
|
215
215
|
/* tooltipText 만 주고 useDescriptionTooltip 을 안 켜면 글이 어디에도 안 떴다 — 그때는 title(가리키면 뜨고 낭독기엔 설명)로 준다 */
|
|
216
|
-
title: !useDescriptionTooltip && tooltipText ? (Array.isArray(tooltipText) ? tooltipText.filter(Boolean).join('\n') : tooltipText) || undefined : undefined, children: [_jsxs("div", { className: "training-block-button__left", children: [showTypeLabel && type?.label && type.label !== String(itemTitle) && (_jsx("span", { className: "training-block-button__type-label caption-1", children: type.label })), isSmall && trainingTypeName && trainingTypeName !== String(itemTitle) && (_jsx("span", { className: "training-block-button__type-label caption-2", children: trainingTypeName })), _jsx(CategoryIcon, { icon: iconForBtn, label: itemTitle, tooltip: popupIconTooltip, wrapperStyle: wrapperStyle, iconSize: (isSmall || isCompact) ? 'small' : 'medium' }), !hideCheckBadge && (isCompleted || isSelected) && !(onRemove && btnHover) && (_jsx(Badge, { className: onRemove ? 'training-block-button__check--removable' : undefined, type: "dot", icon: "check", position: "top-left", size: "medium", "aria-label": isCompleted ? t('training.completed', '훈련 완료') : t('training.selected', '선택됨') })), onRemove && (_jsx(Badge, { className: `training-block-button__remove${btnHover ? ' is-hover' : ''}`, type: "dot", icon: "remove", position: "top-left", size: "medium", "aria-label": t('common.remove', '제거'), onClick: handleRemoveClick })), _jsx("div", { className: `training-block-button__title ${isCompact ? 'body-3' : isSmall ? 'body-3b' : 'h3'}`, children: String(itemTitle) }), _jsxs("div", { className: "training-block-button__description", children: [
|
|
216
|
+
title: !useDescriptionTooltip && tooltipText ? (Array.isArray(tooltipText) ? tooltipText.filter(Boolean).join('\n') : tooltipText) || undefined : undefined, children: [_jsxs("div", { className: "training-block-button__left", children: [showTypeLabel && type?.label && type.label !== String(itemTitle) && (_jsx("span", { className: "training-block-button__type-label caption-1", children: type.label })), isSmall && trainingTypeName && trainingTypeName !== String(itemTitle) && (_jsx("span", { className: "training-block-button__type-label caption-2", children: trainingTypeName })), _jsx(CategoryIcon, { icon: iconForBtn, label: itemTitle, tooltip: popupIconTooltip, wrapperStyle: wrapperStyle, iconSize: (isSmall || isCompact) ? 'small' : 'medium' }), !hideCheckBadge && (isCompleted || isSelected) && !(onRemove && btnHover) && (_jsx(Badge, { className: onRemove ? 'training-block-button__check--removable' : undefined, type: "dot", icon: "check", position: "top-left", size: "medium", "aria-label": isCompleted ? t('training.completed', '훈련 완료') : t('training.selected', '선택됨') })), onRemove && (_jsx(Badge, { className: `training-block-button__remove${btnHover ? ' is-hover' : ''}`, type: "dot", icon: "remove", position: "top-left", size: "medium", "aria-label": t('common.remove', '제거'), onClick: handleRemoveClick })), _jsx("div", { className: `training-block-button__title ${isCompact ? 'body-3' : isSmall ? 'body-3b' : 'h3'}`, children: String(itemTitle) }), _jsxs("div", { className: "training-block-button__description", children: [shownCount ? (_jsx("div", { className: `training-block-button__count ${isSmall ? 'caption-2' : 'caption-1'}`, children: shownCount })) : null, _jsx("div", { className: "training-block-button__chevron", children: _jsx(Icon, { icon: "chevron-right", label: t('training.next', '다음'), className: "training-block-button__chevron-icon", imgClassName: "training-block-button__chevron-icon-image", maskClassName: "training-block-button__chevron-icon-mask", svgClassName: "training-block-button__chevron-icon-svg", size: isSmall ? 12 : 20 }) })] }), ((isList && !isRow) || isSmall) && previewUrls.length > 0 && (_jsx("div", { ref: previewBtnRef, className: "training-block-button__preview", "data-tooltip-ignore": true, onClick: handlePreviewAreaClick, onPointerEnter: handlePreviewHoverOn, onPointerLeave: handlePreviewHoverClose, children: _jsx(Button, { htmlType: "button", size: isSmall ? 28 : 32, type: "dot", icon: playing ? 'pause' : 'play', variant: isSmall ? 'transparent-lightGray' : 'default', useBorder: false, selected: playing,
|
|
217
217
|
/* **칸 이름을 넣는다**(말귀 요청 (54)②, 2026-10-02). 묶음 칸이 스물넷이면
|
|
218
218
|
낭독기로 단추 목록을 불렀을 때 `미리 듣기` 가 **스물네 번** 선다. 바로 옆
|
|
219
219
|
'더보기' 는 ㊺ 에서 이미 칸 이름을 달았는데 이쪽만 안 따라왔다 — 고칠 값은
|
|
@@ -4,13 +4,13 @@ import { useLayoutEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { cx } from '../../utils/cx';
|
|
5
5
|
import { balancedColumns, columnsFitting } from '../../utils/blockGrid';
|
|
6
6
|
import './TrainingBlock.css';
|
|
7
|
-
function
|
|
7
|
+
function countChildren(children) {
|
|
8
8
|
let n = 0;
|
|
9
|
-
const
|
|
10
|
-
c.forEach(
|
|
9
|
+
const walk = (c) => { if (Array.isArray(c))
|
|
10
|
+
c.forEach(walk);
|
|
11
11
|
else if (c != null && c !== false && c !== true && c !== '')
|
|
12
12
|
n++; };
|
|
13
|
-
|
|
13
|
+
walk(children);
|
|
14
14
|
return n;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
@@ -30,36 +30,36 @@ function 자식수(children) {
|
|
|
30
30
|
*/
|
|
31
31
|
export default function TrainingBlockGrid({ gap = 10, count, className, children }) {
|
|
32
32
|
const ref = useRef(null);
|
|
33
|
-
const [
|
|
34
|
-
const
|
|
33
|
+
const [measured, setMeasured] = useState({ width: 0, card: 0 });
|
|
34
|
+
const itemCount = count ?? countChildren(children);
|
|
35
35
|
useLayoutEffect(() => {
|
|
36
36
|
const el = ref.current;
|
|
37
37
|
if (!el)
|
|
38
38
|
return;
|
|
39
|
-
const
|
|
39
|
+
const measure = () => {
|
|
40
40
|
/* 걸어 둔 max-width 가 제 폭을 가리지 않게 잠깐 푼다 */
|
|
41
|
-
const
|
|
41
|
+
const before = el.style.maxWidth;
|
|
42
42
|
el.style.maxWidth = 'none';
|
|
43
|
-
const
|
|
44
|
-
el.style.maxWidth =
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
43
|
+
const width = el.clientWidth;
|
|
44
|
+
el.style.maxWidth = before;
|
|
45
|
+
const firstChild = el.firstElementChild;
|
|
46
|
+
const card = firstChild?.offsetWidth ?? 0;
|
|
47
|
+
setMeasured(prev => (prev.width === width && prev.card === card ? prev : { width, card }));
|
|
48
48
|
};
|
|
49
|
-
|
|
49
|
+
measure();
|
|
50
50
|
if (typeof ResizeObserver === 'undefined')
|
|
51
51
|
return;
|
|
52
|
-
const ro = new ResizeObserver(
|
|
52
|
+
const ro = new ResizeObserver(measure);
|
|
53
53
|
ro.observe(el);
|
|
54
54
|
return () => ro.disconnect();
|
|
55
|
-
}, [
|
|
56
|
-
const
|
|
57
|
-
const
|
|
55
|
+
}, [itemCount, gap]);
|
|
56
|
+
const cardWidth = measured.card > 0 ? measured.card : 0;
|
|
57
|
+
const perRow = cardWidth > 0 ? columnsFitting(measured.width, cardWidth, gap) : 0;
|
|
58
58
|
/* 아직 못 쟀으면(카드가 없거나 첫 틀) 안 건다 — 0 으로 걸면 다 숨는다 */
|
|
59
|
-
const
|
|
59
|
+
const columns = perRow > 0 ? balancedColumns(itemCount, perRow) : 0;
|
|
60
60
|
const style = {
|
|
61
61
|
'--block-grid-gap': `${gap}px`,
|
|
62
|
-
...(
|
|
62
|
+
...(columns > 0 ? { '--block-grid-cols': columns, '--block-grid-card': `${cardWidth}px` } : {}),
|
|
63
63
|
};
|
|
64
|
-
return (_jsx("div", { ref: ref, className: cx('training-block-grid',
|
|
64
|
+
return (_jsx("div", { ref: ref, className: cx('training-block-grid', columns > 0 && 'training-block-grid--measured', className), style: style, "data-cols": columns > 0 ? columns : undefined, children: children }));
|
|
65
65
|
}
|
|
@@ -110,7 +110,19 @@
|
|
|
110
110
|
|
|
111
111
|
.training-card__description {
|
|
112
112
|
margin: 0;
|
|
113
|
-
|
|
113
|
+
/* **곁들이는 줄이라 제목보다 흐리다**(사장님 지시 2026-10-13, 말귀_2 청).
|
|
114
|
+
제목이 `--color-darkGray`(#434d56)인데 설명이 `--color-gray`(#64748b)로 **두 급 차이가
|
|
115
|
+
한 급도 안 돼** 제목과 설명이 한 덩어리로 읽혔다. 한 급 내려 제목이 먼저 잡히게 한다.
|
|
116
|
+
|
|
117
|
+
> **흰 바탕 2.53:1 로 본문 기준(4.5)에 못 미치는 것을 알고 그렇게 둔다.** 이 색이 닿는
|
|
118
|
+
> 것은 **문자열 한 줄로 거드는 설명**뿐이다 — 본문이 필요한 자리는 `description` 에
|
|
119
|
+
> `ReactNode` 를 넘기고, 그때는 이 클래스가 **아예 안 붙는다**(`TrainingCard.tsx` 의
|
|
120
|
+
> `typeof description === 'string'` 가름). 그래서 소비처가 열 몇이어도 무게는
|
|
121
|
+
> `WordTableButton` 설명 줄과 같다. 사장님이 그 셈을 보신 뒤 기본값으로 정하셨다.
|
|
122
|
+
|
|
123
|
+
이 줄을 본문으로 승격하거나 옮길 일이 생기면 **대비를 먼저 다시 본다** —
|
|
124
|
+
`--color-gray`(4.8:1)가 그때의 기본이다. 코어에는 대비를 세는 못이 없어 사람이 본다. */
|
|
125
|
+
color: var(--color-lightGray);
|
|
114
126
|
text-align: left;
|
|
115
127
|
/* 폰트는 body-3 토큰(14px/22px) 그대로 사용 — override 제거 */
|
|
116
128
|
/* 한 줄 말줄임 + 빈 설명도 한 줄 높이 유지(설명 유무로 카드 높이 불변) */
|
|
@@ -5,7 +5,7 @@ import './TrainingCard.css';
|
|
|
5
5
|
import { useCallback } from 'react';
|
|
6
6
|
/** @public */
|
|
7
7
|
export default function TrainingCard({ title, titleLevel = 3, description, topActions, children, footerLeft, footerRight, ariaLabel, onClick, draggable, onDragStart, onDragEnd, className, }) {
|
|
8
|
-
const
|
|
8
|
+
const TitleTag = titleLevel ? `h${titleLevel}` : 'div';
|
|
9
9
|
const hasFooter = footerLeft != null || footerRight != null;
|
|
10
10
|
const hasBody = children != null && children !== false;
|
|
11
11
|
const handleKeyDown = useCallback((e) => {
|
|
@@ -21,7 +21,7 @@ export default function TrainingCard({ title, titleLevel = 3, description, topAc
|
|
|
21
21
|
}
|
|
22
22
|
}, [onClick]);
|
|
23
23
|
return (_jsxs("div", { className: cx('training-card', onClick ? 'training-card--clickable' : '', className), onClick: onClick, onKeyDown: onClick ? handleKeyDown : undefined, role: onClick ? 'button' : undefined, "aria-label": onClick ? (ariaLabel ?? (typeof title === 'string' ? title : undefined)) : undefined, tabIndex: onClick ? 0 : undefined, draggable: draggable, onDragStart: onDragStart, onDragEnd: onDragEnd, children: [_jsxs("div", { className: "training-card__header", children: [_jsxs("div", { className: "training-card__title-row", children: [typeof title === 'string'
|
|
24
|
-
? _jsx(
|
|
24
|
+
? _jsx(TitleTag, { className: "training-card__title h4", children: title })
|
|
25
25
|
: title, topActions != null && (_jsx("div", { className: "training-card__top-actions", onClick: (e) => e.stopPropagation(), children: topActions }))] }), description != null
|
|
26
26
|
? (typeof description === 'string'
|
|
27
27
|
? _jsx("p", { className: "training-card__description body-3", children: description })
|
|
@@ -9,24 +9,24 @@ import { useId } from 'react';
|
|
|
9
9
|
/** @public */
|
|
10
10
|
export default function TrainingCardGroup({ title, titleLevel = 3, count, collapsed = false, onToggle, beforeSort, sort, actions, empty = false, emptyContent, dragOver = false, onDragOver, onDragLeave, onDrop, children, className, }) {
|
|
11
11
|
const { t } = useTranslation();
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const titleId = useId();
|
|
13
|
+
const TitleTag = titleLevel ? `h${titleLevel}` : 'span';
|
|
14
14
|
/* 접을 수 있는가 — `onToggle` 유무가 그것을 말한다. 새 prop 을 두지 않는다. */
|
|
15
|
-
const
|
|
16
|
-
const cls = cx('training-card-group', dragOver && 'training-card-group--dragover', collapsed && 'training-card-group--collapsed',
|
|
15
|
+
const collapsible = onToggle != null;
|
|
16
|
+
const cls = cx('training-card-group', dragOver && 'training-card-group--dragover', collapsed && 'training-card-group--collapsed', collapsible && 'training-card-group--collapsible', className);
|
|
17
17
|
return (
|
|
18
18
|
/* **제목이 이 묶음의 이름이 된다**(말귀 요청 (54)①). 이름 없는 묶음은 지역으로 안
|
|
19
19
|
잡혀, 안의 `접기`·`정렬` 이 그룹마다 같은 이름으로 서도 가릴 길이 없었다 —
|
|
20
20
|
`ChartSection` 에 (51) 로 한 것과 같은 자다. 글자 제목일 때만 건다(마디를 가리키므로
|
|
21
21
|
글이 아닌 제목이면 이름이 안 선다). */
|
|
22
|
-
_jsxs("div", { className: cls, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, role: typeof title === 'string' && title ? 'group' : undefined, "aria-labelledby": typeof title === 'string' && title ?
|
|
22
|
+
_jsxs("div", { className: cls, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, role: typeof title === 'string' && title ? 'group' : undefined, "aria-labelledby": typeof title === 'string' && title ? titleId : undefined, children: [_jsxs("header", { className: "training-card-group__header", children: [collapsible && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down',
|
|
23
23
|
/* **묶음 이름을 단다** — 그룹이 셋 서면 `접기` 도 셋이라 단추 목록에서 못 가린다.
|
|
24
24
|
지역(`role="group"`)이 가려 주지 않는다(위 `titleLevel` 머리글). 제목이 글자일
|
|
25
25
|
때만 — 마디 제목이면 이름을 못 짓는다. */
|
|
26
26
|
ariaLabel: typeof title === 'string' && title
|
|
27
27
|
? (collapsed ? t('common.expandFor', { name: title }) : t('common.collapseFor', { name: title }))
|
|
28
28
|
: (collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기')), className: "training-card-group__chevron", "aria-expanded": !collapsed, onClick: onToggle })), typeof title === 'string'
|
|
29
|
-
? _jsx(
|
|
29
|
+
? _jsx(TitleTag, { id: titleId, className: "training-card-group__title h5b", children: title })
|
|
30
30
|
: title, count !== undefined && _jsx("span", { className: "training-card-group__count body-3b", children: count }), (beforeSort != null || sort || actions != null) && (_jsxs("span", { className: "training-card-group__actions", onClick: e => e.stopPropagation(), children: [beforeSort, sort && (_jsx(Dropdown, { size: 40, options: sort.options, value: sort.value, placeholder: sort.placeholder, onChange: sort.onChange,
|
|
31
31
|
/* 접기와 같은 까닭으로 묶음 이름을 단다 — 그룹마다 `정렬` 이 하나씩 선다 */
|
|
32
32
|
ariaLabel: typeof title === 'string' && title ? t('common.sortFor', { name: title }) : undefined })), actions] }))] }), !collapsed && (empty
|