@ihabdevteam/core 0.322.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 +517 -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 +54 -16
- 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/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.js +6 -13
- 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/DeviceSideTable.js +18 -25
- package/dist/packages/core/src/components/Popup/ListPopup.js +3 -3
- package/dist/packages/core/src/components/Popup/LoadTablePopup.js +7 -7
- 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.js +12 -12
- package/dist/packages/core/src/components/Training/NoiseSettingsTooltip.js +2 -2
- package/dist/packages/core/src/components/Training/TrainingBlock.js +11 -11
- 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 +81 -81
- package/dist/packages/core/src/components/Training/TrainingPagePopupBottom.js +17 -17
- package/dist/packages/core/src/components/Training/TrainingStepAudioPlayer.js +127 -127
- 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 +12 -12
- 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.js +13 -13
- package/dist/packages/core/src/components/Training/trainingMissStats.js +20 -20
- 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 +10 -10
- 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 +15 -15
- package/dist/packages/core/src/components/Word/wordTablePrint.js +10 -10
- package/dist/packages/core/src/components/Word/wordTableRecommend.js +11 -11
- 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 +17 -4
- 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.js +13 -13
- 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/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.js +3 -3
- 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 +0 -11
- package/dist/packages/core/src/hooks/useTablePaging.js +29 -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.js +7 -7
- 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.js +4 -4
- 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.js +14 -14
- 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.js +2 -2
- 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/safeHref.js +4 -4
- package/dist/packages/core/src/utils/savedWordTables.js +5 -5
- package/dist/packages/core/src/utils/speak.js +12 -12
- package/dist/packages/core/src/utils/textSort.d.ts +0 -10
- package/dist/packages/core/src/utils/textSort.js +38 -48
- 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.js +45 -45
- 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
|
@@ -25,30 +25,30 @@ import './SectionHeadFloat.css';
|
|
|
25
25
|
*/
|
|
26
26
|
export default function SectionHeadFloat({ children, className }) {
|
|
27
27
|
const ref = useRef(null);
|
|
28
|
-
const [
|
|
29
|
-
useEffect(() => {
|
|
28
|
+
const [parent, setParent] = useState(null);
|
|
29
|
+
useEffect(() => { setParent(ref.current?.parentElement ?? null); }, []);
|
|
30
30
|
useEffect(() => {
|
|
31
31
|
const el = ref.current;
|
|
32
|
-
if (!el ||
|
|
32
|
+
if (!el || !parent)
|
|
33
33
|
return;
|
|
34
34
|
/* 흐름으로 돌아간 좁은 화면에서는 비울 자리가 없다 — 그때 폭을 적어 두면 제목이 공연히 좁아진다.
|
|
35
35
|
`position` 을 보고 가른다(문턱을 두 군데 적지 않으려고 — CSS 가 정본이다). */
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const w =
|
|
36
|
+
const apply = () => {
|
|
37
|
+
const isFloating = typeof window !== 'undefined' && window.getComputedStyle(el).position === 'absolute';
|
|
38
|
+
const w = isFloating ? Math.ceil(el.getBoundingClientRect().width) : 0;
|
|
39
39
|
if (w > 0)
|
|
40
|
-
|
|
40
|
+
parent.style.setProperty('--section-head-float-w', `${w}px`);
|
|
41
41
|
else
|
|
42
|
-
|
|
42
|
+
parent.style.removeProperty('--section-head-float-w');
|
|
43
43
|
};
|
|
44
|
-
|
|
44
|
+
apply();
|
|
45
45
|
if (typeof ResizeObserver === 'undefined')
|
|
46
|
-
return () => {
|
|
46
|
+
return () => { parent.style.removeProperty('--section-head-float-w'); };
|
|
47
47
|
/* 줄 자신과 카드를 함께 본다 — 단추 글자가 바뀌면 줄이, 창이 좁아지면 카드가 먼저 움직인다 */
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return () => {
|
|
52
|
-
}, [
|
|
48
|
+
const observer = new ResizeObserver(() => apply());
|
|
49
|
+
observer.observe(el);
|
|
50
|
+
observer.observe(parent);
|
|
51
|
+
return () => { observer.disconnect(); parent.style.removeProperty('--section-head-float-w'); };
|
|
52
|
+
}, [parent, children]);
|
|
53
53
|
return _jsx("div", { ref: ref, className: cx('section-head-float', className), children: children });
|
|
54
54
|
}
|
|
@@ -18,7 +18,7 @@ import { devWarnOnce, isDev } from '../utils/devWarn';
|
|
|
18
18
|
* 멀쩡하다) 콘솔이 울기만 하고, 정작 부딪힌 자리가 묻힌다. 둘 이상 **함께 서 있을 때만**
|
|
19
19
|
* 알린다. 열쇠에 보기 이름을 넣어, 서로 다른 부딪힘은 각각 한 번씩 알린다.
|
|
20
20
|
*/
|
|
21
|
-
const
|
|
21
|
+
const unlabeledGroups = new Map();
|
|
22
22
|
/** @public */
|
|
23
23
|
export default function SegmentedControl(props) {
|
|
24
24
|
const { t } = useTranslation();
|
|
@@ -31,69 +31,69 @@ export default function SegmentedControl(props) {
|
|
|
31
31
|
아무 일도 안 일어난다. 실측으로 칸 수만큼 Tab 자리였고 ArrowRight 는 죽어 있었다.
|
|
32
32
|
라디오에서는 **고름이 포커스를 따라간다** — 화살표로 옮기면 그 자리가 곧 선택이다.
|
|
33
33
|
못 고르는 칸(disabled)은 건너뛰고, 끝에서는 돈다. */
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
34
|
+
const groupRef = useRef(null);
|
|
35
|
+
const selectable = options.filter((o) => !o.disabled);
|
|
36
|
+
const uid = useId();
|
|
37
37
|
/* 보기 이름을 이어 **이 고르개가 무엇인지** 적는다 — 콘솔만 보고 찾아갈 수 있게.
|
|
38
38
|
원시 글자라 아래 useEffect 가 다시 그릴 때마다 돌지 않는다(배열을 그냥 넣으면 돈다). */
|
|
39
|
-
const
|
|
39
|
+
const signature = options.map((o) => o.key).join('·');
|
|
40
40
|
useEffect(() => {
|
|
41
41
|
if (!isDev || (ariaLabel != null && ariaLabel !== ''))
|
|
42
42
|
return;
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
const
|
|
46
|
-
devWarnOnce(`segmented-control-unnamed:${
|
|
43
|
+
unlabeledGroups.set(uid, signature || '(보기 없음)');
|
|
44
|
+
if (unlabeledGroups.size > 1) {
|
|
45
|
+
const all = [...unlabeledGroups.values()].sort();
|
|
46
|
+
devWarnOnce(`segmented-control-unnamed:${all.join('|')}`, `[ihab/SegmentedControl] 이름 없는 라디오그룹이 한 화면에 ${unlabeledGroups.size} 개 서 있습니다 — ` +
|
|
47
47
|
`낭독기는 모두 "선택 탭" 으로 읽어 어느 것이 무엇인지 가릴 수 없습니다. ariaLabel 을 주세요. ` +
|
|
48
|
-
`지금 선 것(보기 이름으로 적음): ${
|
|
48
|
+
`지금 선 것(보기 이름으로 적음): ${all.join(' / ')}`);
|
|
49
49
|
}
|
|
50
|
-
return () => {
|
|
51
|
-
}, [ariaLabel,
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
50
|
+
return () => { unlabeledGroups.delete(uid); };
|
|
51
|
+
}, [ariaLabel, signature, uid]);
|
|
52
|
+
const moveBy = (step) => {
|
|
53
|
+
if (!selectable.length)
|
|
54
54
|
return;
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
:
|
|
58
|
-
:
|
|
59
|
-
if (
|
|
55
|
+
const current = Math.max(0, selectable.findIndex((o) => o.key === value));
|
|
56
|
+
const next = step === 'first' ? selectable[0]
|
|
57
|
+
: step === 'last' ? selectable[selectable.length - 1]
|
|
58
|
+
: selectable[(current + step + selectable.length) % selectable.length];
|
|
59
|
+
if (!next)
|
|
60
60
|
return;
|
|
61
61
|
/* 이미 그 자리여도 포커스는 옮긴다 — 칸이 하나뿐일 때 멈춘 것처럼 보이지 않게 */
|
|
62
|
-
onChange(
|
|
63
|
-
const
|
|
64
|
-
.find((el) => el.dataset.segkey ===
|
|
65
|
-
|
|
62
|
+
onChange(next.key);
|
|
63
|
+
const buttons = Array.from(groupRef.current?.querySelectorAll('[data-segkey]') ?? [])
|
|
64
|
+
.find((el) => el.dataset.segkey === next.key);
|
|
65
|
+
buttons?.focus();
|
|
66
66
|
};
|
|
67
|
-
const
|
|
67
|
+
const onKeyDown = (e) => {
|
|
68
68
|
switch (e.key) {
|
|
69
69
|
case 'ArrowRight':
|
|
70
70
|
case 'ArrowDown':
|
|
71
|
-
|
|
71
|
+
moveBy(1);
|
|
72
72
|
break;
|
|
73
73
|
case 'ArrowLeft':
|
|
74
74
|
case 'ArrowUp':
|
|
75
|
-
|
|
75
|
+
moveBy(-1);
|
|
76
76
|
break;
|
|
77
77
|
case 'Home':
|
|
78
|
-
|
|
78
|
+
moveBy('first');
|
|
79
79
|
break;
|
|
80
80
|
case 'End':
|
|
81
|
-
|
|
81
|
+
moveBy('last');
|
|
82
82
|
break;
|
|
83
83
|
default: return;
|
|
84
84
|
}
|
|
85
85
|
e.preventDefault();
|
|
86
86
|
};
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
87
|
+
const labelOf = (opt) => {
|
|
88
|
+
const isText = (v) => typeof v === 'string' || typeof v === 'number';
|
|
89
|
+
if (!isText(opt.label) || opt.count == null || opt.count === false || !isText(opt.count))
|
|
90
90
|
return undefined;
|
|
91
91
|
return `${opt.label}, ${opt.count}`;
|
|
92
92
|
};
|
|
93
93
|
/* Tab 자리를 받을 한 칸 — 고른 것이 있으면 그것, 없으면 고를 수 있는 첫 칸 */
|
|
94
|
-
const
|
|
95
|
-
return (_jsx("div", { ref:
|
|
94
|
+
const tabKey = selectable.some((o) => o.key === value) ? value : selectable[0]?.key;
|
|
95
|
+
return (_jsx("div", { ref: groupRef, onKeyDown: onKeyDown, className: cx('segmented-control', `segmented-control--${shape}`, `segmented-control--${size}`, fullWidth && 'segmented-control--full', !useBG && 'segmented-control--no-bg', minButtonWidth != null && 'segmented-control--min', className), style: minButtonWidth != null ? { ['--segmented-min-button-w']: `${minButtonWidth}px` } : undefined, 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 === tabKey ? 0 : -1, label: opt.label, count: opt.count,
|
|
96
96
|
/* 수를 이름에 넣는다 — 글자 이름표는 단추의 aria-label 이 되어 내용(수)이 이름 계산에서 빠졌다("전체" 로만 읽힘) */
|
|
97
|
-
ariaLabel:
|
|
97
|
+
ariaLabel: labelOf(opt), disabled: opt.disabled, className: opt.key === value ? 'is-selected' : '', onClick: () => { if (!opt.disabled)
|
|
98
98
|
onChange(opt.key); }, size: size, shape: shape, useBorder: false, useBG: false }, opt.key))) }));
|
|
99
99
|
}
|
|
@@ -22,9 +22,9 @@ function SideNavFooter(props) {
|
|
|
22
22
|
넷 서 있었다 — 눌러도 아무 일이 없으니 사람은 고장으로 읽고, 낭독기에는
|
|
23
23
|
'알림 단추' 라고 읽히는데 실제로는 아무 데도 못 간다.
|
|
24
24
|
**없는 단추가 죽은 단추보다 낫다.** */
|
|
25
|
-
const
|
|
25
|
+
const hasAny = onDebug || onNotification || onSetting || onHelp || onFeedback || onAccount;
|
|
26
26
|
// 하나도 없으면 바닥 자체를 비운다 — 빈 채로 두면 여백(margin-top + padding)만 남는다.
|
|
27
|
-
if (
|
|
27
|
+
if (!hasAny)
|
|
28
28
|
return null;
|
|
29
29
|
return (_jsxs("div", { className: "side-nav__footer", children: [onDebug && (_jsx(Button, { htmlType: "button", size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "terminal", ariaLabel: t('nav.debug', '디버그'), onClick: onDebug, selected: activeKey === 'debug' })), onNotification && (_jsxs("span", { className: "side-nav__notification-wrap", children: [_jsx(Button, { htmlType: "button", size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "bell", ariaLabel: nameWithCount(t, t('nav.notifications', '알림'), unreadCount), onClick: onNotification, selected: activeKey === 'notifications' }), !!unreadCount && unreadCount > 0 && (_jsx(Badge, { type: "dot", number: Math.min(unreadCount, 99), size: 24, "aria-hidden": true }))] })), onSetting && (_jsx(Button, { htmlType: "button", size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "settings", ariaLabel: t('nav.settings', '설정'), onClick: onSetting, selected: activeKey === 'settings' })), onHelp && (_jsx(Button, { htmlType: "button", size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "message-circle-question-mark", ariaLabel: t('nav.help', '도움말'), onClick: onHelp, selected: activeKey === 'help' })), onFeedback && (_jsx(Button, { htmlType: "button", size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "bug", ariaLabel: t('nav.feedback', '버그신고'), onClick: onFeedback, selected: activeKey === 'feedback' })), onAccount && (_jsx(Button, { htmlType: "button", size: 48, showTooltipInfo: true, type: "dot", shape: "pill", variant: variant, icon: "user-round", ariaLabel: t('nav.account', '계정'), onClick: onAccount, selected: activeKey === 'account' }))] }));
|
|
30
30
|
}
|
|
@@ -65,76 +65,76 @@ export default function SideNav(props) {
|
|
|
65
65
|
const handleModeSwitch = useCallback(() => onAppModeChange?.(isManager ? 'malgwi' : 'manager'), [onAppModeChange, isManager]);
|
|
66
66
|
/* **떠 있는 막대의 자리와 길이** — 구르는 통(이 `<nav>`)을 재어 셈한다. 네이티브 막대는
|
|
67
67
|
폭을 정할 수 없고 자리를 먹어 항목을 밀어내므로(2026-09-25 실측: 오른쪽만 23px) 직접 그린다. */
|
|
68
|
-
const
|
|
69
|
-
const [
|
|
68
|
+
const containerRef = useRef(null);
|
|
69
|
+
const [thumb, setThumb] = useState(null);
|
|
70
70
|
useEffect(() => {
|
|
71
|
-
const el =
|
|
71
|
+
const el = containerRef.current;
|
|
72
72
|
if (!el)
|
|
73
73
|
return;
|
|
74
74
|
/* **굴림 한 번에 한 번만 센다**(말귀 요청 (115), 2026-10-10). 전에는 `scroll` 마다 바로 `setState` 라
|
|
75
75
|
굴리는 동안 틀마다 여러 번 다시 그렸다. 틀에 한 번으로 모으고(rAF), **값이 그대로면 상태를 안
|
|
76
76
|
건드린다** — 같은 수를 새 객체로 넘기면 React 는 바뀐 줄 안다. */
|
|
77
|
-
let
|
|
78
|
-
const
|
|
77
|
+
let rafId = null;
|
|
78
|
+
const compute = () => {
|
|
79
79
|
const { scrollHeight, clientHeight, scrollTop } = el;
|
|
80
80
|
/* 넘치지 않으면 그리지 않는다 — 없는 스크롤을 알릴 까닭이 없다 */
|
|
81
81
|
if (scrollHeight <= clientHeight + 1) {
|
|
82
|
-
|
|
82
|
+
setThumb(prev => (prev === null ? prev : null));
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
85
|
+
const thumbHeight = Math.max(24, (clientHeight / scrollHeight) * clientHeight);
|
|
86
|
+
const trackRemain = clientHeight - thumbHeight;
|
|
87
|
+
const scrollRemain = scrollHeight - clientHeight;
|
|
88
|
+
const next = { top: Math.round((scrollTop / scrollRemain) * trackRemain), height: Math.round(thumbHeight) };
|
|
89
|
+
setThumb(prev => (prev && prev.top === next.top && prev.height === next.height ? prev : next));
|
|
90
90
|
};
|
|
91
|
-
const
|
|
91
|
+
const measure = () => {
|
|
92
92
|
if (typeof requestAnimationFrame === 'undefined') {
|
|
93
|
-
|
|
93
|
+
compute();
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
|
-
if (
|
|
96
|
+
if (rafId !== null)
|
|
97
97
|
return;
|
|
98
|
-
|
|
98
|
+
rafId = requestAnimationFrame(() => { rafId = null; compute(); });
|
|
99
99
|
};
|
|
100
|
-
|
|
101
|
-
el.addEventListener('scroll',
|
|
100
|
+
compute();
|
|
101
|
+
el.addEventListener('scroll', measure, { passive: true });
|
|
102
102
|
/* 항목이 늘거나 창이 바뀌면 길이도 달라진다 */
|
|
103
|
-
const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(
|
|
103
|
+
const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(measure) : null;
|
|
104
104
|
ro?.observe(el);
|
|
105
105
|
for (const c of Array.from(el.children))
|
|
106
106
|
ro?.observe(c);
|
|
107
107
|
return () => {
|
|
108
|
-
if (
|
|
109
|
-
cancelAnimationFrame(
|
|
110
|
-
el.removeEventListener('scroll',
|
|
108
|
+
if (rafId !== null)
|
|
109
|
+
cancelAnimationFrame(rafId);
|
|
110
|
+
el.removeEventListener('scroll', measure);
|
|
111
111
|
ro?.disconnect();
|
|
112
112
|
};
|
|
113
113
|
}, [items]);
|
|
114
114
|
/* **막대를 잡아 끌 수 있다.** 손에 잡히게 그려 놓고 안 잡히면 그 꼴이 거짓말이 된다.
|
|
115
115
|
통의 한 픽셀은 막대의 `scrollHeight / clientHeight` 배다 — 그 비로 옮긴다. */
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const el =
|
|
116
|
+
const dragRef = useRef(null);
|
|
117
|
+
const onThumbDown = useCallback((e) => {
|
|
118
|
+
const el = containerRef.current;
|
|
119
119
|
if (!el)
|
|
120
120
|
return;
|
|
121
121
|
e.preventDefault();
|
|
122
|
-
|
|
122
|
+
dragRef.current = { y: e.clientY, top: el.scrollTop };
|
|
123
123
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
124
124
|
}, []);
|
|
125
|
-
const
|
|
126
|
-
const el =
|
|
127
|
-
const
|
|
128
|
-
if (!el ||
|
|
125
|
+
const onThumbMove = useCallback((e) => {
|
|
126
|
+
const el = containerRef.current;
|
|
127
|
+
const held = dragRef.current;
|
|
128
|
+
if (!el || !held)
|
|
129
129
|
return;
|
|
130
|
-
el.scrollTop =
|
|
130
|
+
el.scrollTop = held.top + (e.clientY - held.y) * (el.scrollHeight / el.clientHeight);
|
|
131
131
|
}, []);
|
|
132
|
-
const
|
|
133
|
-
|
|
132
|
+
const onThumbUp = useCallback((e) => {
|
|
133
|
+
dragRef.current = null;
|
|
134
134
|
if (e.currentTarget.hasPointerCapture(e.pointerId))
|
|
135
135
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
136
136
|
}, []);
|
|
137
|
-
return (_jsxs("nav", { ref:
|
|
137
|
+
return (_jsxs("nav", { ref: containerRef, "aria-label": t('nav.mainMenu', '메인 메뉴'), className: "side-nav__menu", children: [thumb && (_jsx("div", { className: "side-nav__scroll", "aria-hidden": "true", children: _jsx("div", { className: "side-nav__scroll-thumb", style: { top: thumb.top, height: thumb.height }, onPointerDown: onThumbDown, onPointerMove: onThumbMove, onPointerUp: onThumbUp, onPointerCancel: onThumbUp }) })), _jsxs("div", { className: "side-nav__menu-list", children: [(items || [])
|
|
138
138
|
.filter((it) => !it.section && (it.key || it.label))
|
|
139
139
|
.map((it, idx) => (_jsxs(React.Fragment, { children: [it.divider && _jsx("div", { className: "side-nav__separator" }), _jsx(SideNavButton, { item: it, active: !!it.key && it.key === activeKey, state: it.disabled ? 'disabled' : undefined, onClick: (_key, btnEl, fromPointer) => {
|
|
140
140
|
if (it.disabled)
|
|
@@ -174,8 +174,8 @@ export default function SideNav(props) {
|
|
|
174
174
|
const supportsMatches = typeof headerEl.matches === 'function';
|
|
175
175
|
/* :hover 는 **호버가 되는 기기에서만** 믿는다 — iPad 는 톡한 요소에 :hover 가 붙어 남아, "마우스가 레일 위에 있다" 로
|
|
176
176
|
읽혀 강제 접힘을 건너뛰었다(메뉴를 골라도 레일이 펼친 채 본문을 가림) */
|
|
177
|
-
const
|
|
178
|
-
const isHovered =
|
|
177
|
+
const hasHover = window.matchMedia('(hover: hover)').matches;
|
|
178
|
+
const isHovered = hasHover && supportsMatches ? headerEl.matches(':hover') : false;
|
|
179
179
|
if (!isHovered) {
|
|
180
180
|
headerEl.classList.add('header--force-collapsed');
|
|
181
181
|
const onEnter = () => {
|
|
@@ -87,7 +87,7 @@ export default function Slider({ value, onChange, min = 0, max = 100, step = 1,
|
|
|
87
87
|
}
|
|
88
88
|
/* 낭독기가 들을 값 글자 — 숫자만 넘기면 "1.25x"·"위"·"3 / 7" 이 "1.25"·"0"·"3" 으로 들렸다. 보이는 라벨과 같은 글자,
|
|
89
89
|
라벨이 없으면 끝값의 minLabel·maxLabel. 둘 다 아니면 숫자 그대로(undefined). */
|
|
90
|
-
const
|
|
90
|
+
const valueFormatter = formatValue
|
|
91
91
|
? formatValue(clamped)
|
|
92
92
|
: label === 'percent' ? `${Math.round(max > min ? ((clamped - min) / (max - min)) * 100 : 0)}%`
|
|
93
93
|
: label === 'count' ? `${clamped} / ${max}`
|
|
@@ -95,7 +95,7 @@ export default function Slider({ value, onChange, min = 0, max = 100, step = 1,
|
|
|
95
95
|
: clamped === max && maxLabel ? maxLabel
|
|
96
96
|
: undefined;
|
|
97
97
|
// 루트가 아닐 때(label/showMinMax로 감싸질 때)는 className을 바깥 wrapper가 대신 가져간다
|
|
98
|
-
const input = (_jsx("input", { type: "range", className: cx('slider', `slider--${orientation}`, isStatic && 'slider--static', !label && !showMinMax && className), min: min, max: max, step: step, value: clamped, disabled: disabled, readOnly: isStatic, tabIndex: isStatic ? -1 : undefined, "aria-label": ariaLabel, "aria-valuetext":
|
|
98
|
+
const input = (_jsx("input", { type: "range", className: cx('slider', `slider--${orientation}`, isStatic && 'slider--static', !label && !showMinMax && className), min: min, max: max, step: step, value: clamped, disabled: disabled, readOnly: isStatic, tabIndex: isStatic ? -1 : undefined, "aria-label": ariaLabel, "aria-valuetext": valueFormatter, "aria-orientation": orientation, onChange: handleChange, style: style }));
|
|
99
99
|
if (!label && !showMinMax)
|
|
100
100
|
return input;
|
|
101
101
|
// 트랙 양 끝 최소·최대값 텍스트 — 세로는 채움 방향(아래→위)에 맞춰 위=max/아래=min 순서로 배치
|
|
@@ -16,7 +16,7 @@ import './SoundCompass.css';
|
|
|
16
16
|
const lerp = (a, b, t) => a + (b - a) * t;
|
|
17
17
|
/** @public */
|
|
18
18
|
export default function SoundCompass({ speakers, size = 300, viewT = 0, activeSpeakerId = null, playedIds, selectedId = null, answerId = null, showFeedback = false, pulseSelected = false, pulseSelectedDuration, pulseScale, clickable = false, onSelectSpeaker, buttonSize = 40, showIcon = true, onCenterClick, centerLabel, centerDisabled = false, centerAttention = false, shortcutAnchor = 'top-right', shortcutScale, scaleShortcut = false, }) {
|
|
19
|
-
const { t:
|
|
19
|
+
const { t: translate } = useTranslation();
|
|
20
20
|
const t = Math.max(0, Math.min(1, viewT));
|
|
21
21
|
const CENTER = size / 2;
|
|
22
22
|
// ── 탑뷰 좌표 ──────────────────────────────────────────────────────────
|
|
@@ -123,10 +123,10 @@ export default function SoundCompass({ speakers, size = 300, viewT = 0, activeSp
|
|
|
123
123
|
const played = useMemo(() => new Set(playedIds ?? []), [playedIds]);
|
|
124
124
|
const stateLabel = (state) => ({
|
|
125
125
|
idle: '',
|
|
126
|
-
active:
|
|
127
|
-
played:
|
|
128
|
-
correct:
|
|
129
|
-
wrong:
|
|
126
|
+
active: translate('soundCompass.state.active', '소리 나는 중'),
|
|
127
|
+
played: translate('soundCompass.state.played', '들음'),
|
|
128
|
+
correct: translate('soundCompass.state.correct', '정답'),
|
|
129
|
+
wrong: translate('soundCompass.state.wrong', '오답'),
|
|
130
130
|
})[state];
|
|
131
131
|
const getSpeakerState = (sp) => {
|
|
132
132
|
if (activeSpeakerId === sp.id)
|