@ihabdevteam/core 0.322.0 → 0.324.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 +557 -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 +55 -17
- 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 +14 -2
- package/dist/packages/core/src/i18n/locales/ko.json +13 -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 +129 -105
- package/dist/packages/core/src/utils/phonemePairSearchTypes.d.ts +2 -0
- 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.d.ts +1 -1
- package/dist/packages/core/src/utils/wordAttributeFilterCodes.js +65 -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
|
@@ -31,7 +31,7 @@ function formatDisplay(iso) {
|
|
|
31
31
|
}
|
|
32
32
|
/* 연월 이름은 `연월이름` 으로 받는다 — 예전에는 `${y}년 ${m + 1}월` 로 박혀 있어 영어 화면에서도
|
|
33
33
|
한국어 연월이 떴다. 부르는 쪽이 사전(`calendar.yearMonth`, Calendar 와 같은 열쇠)을 넘긴다. */
|
|
34
|
-
function buildYMOptions(viewY,
|
|
34
|
+
function buildYMOptions(viewY, ymLabel, min, max) {
|
|
35
35
|
const startY = min ? +min.split('-')[0] : viewY - 10;
|
|
36
36
|
const endY = max ? +max.split('-')[0] : viewY + 10;
|
|
37
37
|
const opts = [];
|
|
@@ -42,7 +42,7 @@ function buildYMOptions(viewY, 연월이름, min, max) {
|
|
|
42
42
|
continue;
|
|
43
43
|
if (max && ym > max.slice(0, 7))
|
|
44
44
|
continue;
|
|
45
|
-
opts.push({ value: ym, label:
|
|
45
|
+
opts.push({ value: ym, label: ymLabel(y, m + 1) });
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
return opts;
|
|
@@ -57,21 +57,21 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
57
57
|
진작 `calendar.dow` 를 쓰고 있었고, 사전에는 영어(`Sun`·`Mon`…)도 들어 있었다. */
|
|
58
58
|
const DOW = useMemo(() => t('calendar.dow', { returnObjects: true }), [t]);
|
|
59
59
|
// 오늘 표시·열 때 포커스는 KST 의 날 — 기기 달력이면 해외 기기에서 하루 어긋나 min=오늘 달력이 앞 달로 열렸다
|
|
60
|
-
const
|
|
60
|
+
const todayIso = todayYmdOr(today);
|
|
61
61
|
const { viewY, viewM, prevMonth, nextMonth, setViewYM } = useCalendarMonth(value, today);
|
|
62
62
|
const panelId = useId();
|
|
63
63
|
const computeStyle = useCallback((rect, trigger) => {
|
|
64
64
|
const vh = window.innerHeight;
|
|
65
65
|
const vw = window.innerWidth;
|
|
66
66
|
/* 판의 실제 크기 — 열린 뒤에 재므로 있다. 없으면(첫 계산 전) 어림값 */
|
|
67
|
-
const
|
|
67
|
+
const panel = typeof document !== 'undefined' ? document.getElementById(panelId) : null;
|
|
68
68
|
/* **우리가 얹은 자리에 흔들리지 않는 높이를 쓴다.**
|
|
69
69
|
`offsetHeight` 만 보면 우리가 방금 얹은 `top`/`bottom` 이 상자를 눌렀을 때 그
|
|
70
70
|
눌린 값을 되읽는다 — 그 값으로 다시 자리를 잡고, 자리가 다시 높이를 바꾸어
|
|
71
71
|
두 꼴을 오간다(실측: 306 ↔ 26). `scrollHeight` 는 눌려도 **안엣것의 높이**를
|
|
72
72
|
그대로 내놓으므로 둘 중 큰 쪽을 쓴다. 판이 아직 없으면 어림값. */
|
|
73
|
-
const dropdownH = Math.max(
|
|
74
|
-
const
|
|
73
|
+
const dropdownH = Math.max(panel?.offsetHeight || 0, panel?.scrollHeight || 0) || 300;
|
|
74
|
+
const panelWidth = Math.max(rect.width, panel?.offsetWidth || 0, 260);
|
|
75
75
|
const spaceBelow = vh - rect.bottom;
|
|
76
76
|
const spaceAbove = rect.top;
|
|
77
77
|
/* **아래가 모자라면 넓은 쪽으로 편다**(말귀 요청 ㊵, 2026-10-01).
|
|
@@ -82,11 +82,11 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
82
82
|
const above = spaceBelow < dropdownH && spaceAbove > spaceBelow;
|
|
83
83
|
/* **창 안으로 조인다** — 스크롤을 따라 다시 잴 때 트리거가 화면 밖으로 나가면 달력이 창 위·아래 밖까지 따라가
|
|
84
84
|
고를 수 없었다. 판이 창보다 크면 위 여백에 붙인다. */
|
|
85
|
-
const
|
|
86
|
-
const
|
|
85
|
+
const margin = 8;
|
|
86
|
+
const clamp = (v, upper) => Math.min(Math.max(margin, v), Math.max(margin, upper));
|
|
87
87
|
return {
|
|
88
88
|
position: 'fixed',
|
|
89
|
-
left:
|
|
89
|
+
left: clamp(rect.left, vw - panelWidth - margin),
|
|
90
90
|
width: rect.width,
|
|
91
91
|
minWidth: 260,
|
|
92
92
|
/* **둘 다 적는다 — 쓰는 쪽만 적으면 안 된다.**
|
|
@@ -95,8 +95,8 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
95
95
|
`top` 이 이기고 높이는 그 둘 사이로 눌린다 — 판이 26px 으로 쪼그라들어
|
|
96
96
|
화면 밖에 섰다(말귀 실측 2026-10-01). `auto` 로 **지운다.** */
|
|
97
97
|
...(above
|
|
98
|
-
? { top: 'auto', bottom:
|
|
99
|
-
: { bottom: 'auto', top:
|
|
98
|
+
? { top: 'auto', bottom: clamp(vh - rect.top + 6, vh - dropdownH - margin) }
|
|
99
|
+
: { bottom: 'auto', top: clamp(rect.bottom + 6, vh - dropdownH - margin) }),
|
|
100
100
|
zIndex: anchoredMenuZIndex(trigger),
|
|
101
101
|
};
|
|
102
102
|
}, [panelId]);
|
|
@@ -122,17 +122,17 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
122
122
|
useEffect(() => {
|
|
123
123
|
if (!open)
|
|
124
124
|
return;
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
125
|
+
const panel = menuRef.current;
|
|
126
|
+
const focusTo = panel?.querySelector('.date-picker__day--selected:not(:disabled)') ??
|
|
127
|
+
panel?.querySelector('.date-picker__day--today:not(:disabled)') ??
|
|
128
128
|
/* 오늘이 min/max 밖이라 꺼져 있으면 focus() 가 실패해 트리거에 남았다 — 켜진 첫 날로 */
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
panel?.querySelector('.date-picker__day:not(:disabled)') ??
|
|
130
|
+
panel?.querySelector(FOCUSABLE_SELECTOR) ??
|
|
131
|
+
panel;
|
|
132
|
+
focusTo?.focus?.();
|
|
133
133
|
return () => {
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
134
|
+
const active = document.activeElement;
|
|
135
|
+
if (!active || active === document.body)
|
|
136
136
|
triggerRef.current?.focus?.();
|
|
137
137
|
};
|
|
138
138
|
}, [open, menuRef, triggerRef]);
|
|
@@ -142,37 +142,37 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
142
142
|
Tab 은 격자를 **하나로** 세고, 그 안은 화살표로 다닌다.
|
|
143
143
|
←→ 하루 · ↑↓ 한 주 · Home/End 그 주의 처음과 끝 · PageUp/Down 한 달 ·
|
|
144
144
|
Shift 를 얹으면 한 해. min/max 를 넘어가면 그 끝으로 잡아 둔다. */
|
|
145
|
-
const [
|
|
146
|
-
const
|
|
145
|
+
const [activeDay, setActiveDay] = useState(1);
|
|
146
|
+
const gridRef = useRef(null);
|
|
147
147
|
/* 옮긴 뒤에 포커스를 준다 — 달을 넘는 이동은 다시 그려진 **뒤**라야 그 칸이
|
|
148
148
|
생긴다. 효과에서 한 번만 집어 가게 쪽지를 남긴다(매 렌더 훔치지 않도록). */
|
|
149
|
-
const
|
|
149
|
+
const focusDay = useRef(null);
|
|
150
150
|
useEffect(() => {
|
|
151
151
|
if (!open)
|
|
152
152
|
return;
|
|
153
|
-
const
|
|
154
|
-
:
|
|
153
|
+
const firstIndex = value && value.slice(0, 7) === `${viewY}-${String(viewM + 1).padStart(2, '0')}` ? +value.slice(8)
|
|
154
|
+
: todayIso.slice(0, 7) === `${viewY}-${String(viewM + 1).padStart(2, '0')}` ? +todayIso.slice(8)
|
|
155
155
|
: 1;
|
|
156
|
-
|
|
156
|
+
setActiveDay(firstIndex);
|
|
157
157
|
// 열 때 한 번만 잡는다 — 그 뒤의 달 이동은 아래 갈무리가 맡는다
|
|
158
158
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
159
159
|
}, [open]);
|
|
160
160
|
useEffect(() => {
|
|
161
|
-
if (
|
|
161
|
+
if (focusDay.current == null)
|
|
162
162
|
return;
|
|
163
|
-
const d =
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
const d = focusDay.current;
|
|
164
|
+
focusDay.current = null;
|
|
165
|
+
gridRef.current?.querySelector(`[data-day="${d}"]`)?.focus();
|
|
166
166
|
});
|
|
167
167
|
/* 달이 바뀌면(나침반 단추나 연월 고르개로) 활성일이 그 달 밖일 수 있다.
|
|
168
168
|
여기서는 **포커스를 안 옮긴다** — 사람은 그 단추 위에 있다. */
|
|
169
169
|
useEffect(() => {
|
|
170
|
-
|
|
170
|
+
setActiveDay((d) => Math.min(d, daysInMonth(viewY, viewM)));
|
|
171
171
|
}, [viewY, viewM]);
|
|
172
172
|
/* 옮길 곳을 날짜로 셈한다 — 날 넘김은 Date 에 맡기고, 달 넘김은 아래에서 말일로 조인다.
|
|
173
173
|
min/max 밖으로 나가면 그 끝으로 잡는다. */
|
|
174
|
-
const
|
|
175
|
-
let iso = toISO(
|
|
174
|
+
const moveTo = useCallback((next) => {
|
|
175
|
+
let iso = toISO(next.getFullYear(), next.getMonth(), next.getDate());
|
|
176
176
|
if (min && iso < min)
|
|
177
177
|
iso = min;
|
|
178
178
|
if (max && iso > max)
|
|
@@ -180,47 +180,47 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
180
180
|
const [y, m, d] = iso.split('-').map(Number);
|
|
181
181
|
if (y !== viewY || m - 1 !== viewM)
|
|
182
182
|
setViewYM(y, m - 1);
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
setActiveDay(d);
|
|
184
|
+
focusDay.current = d;
|
|
185
185
|
}, [min, max, viewY, viewM, setViewYM]);
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
186
|
+
const onGridKeyDown = useCallback((e) => {
|
|
187
|
+
const base = new Date(viewY, viewM, activeDay);
|
|
188
|
+
const byDay = (n) => { base.setDate(base.getDate() + n); moveTo(base); };
|
|
189
189
|
// 달 넘김은 **목표 달의 말일로 조인다** — setMonth 는 말일에서 넘쳐 1/31 → 3/3, 3/31 에서 앞 달로 못 가고, 2028-02-29 → 2029-03-01 이 됐다
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const y = Math.floor(
|
|
193
|
-
|
|
190
|
+
const byMonth = (n) => {
|
|
191
|
+
const total = base.getFullYear() * 12 + base.getMonth() + n;
|
|
192
|
+
const y = Math.floor(total / 12), m = total - y * 12;
|
|
193
|
+
moveTo(new Date(y, m, Math.min(base.getDate(), daysInMonth(y, m))));
|
|
194
194
|
};
|
|
195
195
|
switch (e.key) {
|
|
196
196
|
case 'ArrowLeft':
|
|
197
|
-
|
|
197
|
+
byDay(-1);
|
|
198
198
|
break;
|
|
199
199
|
case 'ArrowRight':
|
|
200
|
-
|
|
200
|
+
byDay(1);
|
|
201
201
|
break;
|
|
202
202
|
case 'ArrowUp':
|
|
203
|
-
|
|
203
|
+
byDay(-7);
|
|
204
204
|
break;
|
|
205
205
|
case 'ArrowDown':
|
|
206
|
-
|
|
206
|
+
byDay(7);
|
|
207
207
|
break;
|
|
208
208
|
case 'Home':
|
|
209
|
-
|
|
209
|
+
byDay(-base.getDay());
|
|
210
210
|
break;
|
|
211
211
|
case 'End':
|
|
212
|
-
|
|
212
|
+
byDay(6 - base.getDay());
|
|
213
213
|
break;
|
|
214
214
|
case 'PageUp':
|
|
215
|
-
e.shiftKey ?
|
|
215
|
+
e.shiftKey ? byMonth(-12) : byMonth(-1);
|
|
216
216
|
break;
|
|
217
217
|
case 'PageDown':
|
|
218
|
-
e.shiftKey ?
|
|
218
|
+
e.shiftKey ? byMonth(12) : byMonth(1);
|
|
219
219
|
break;
|
|
220
220
|
default: return;
|
|
221
221
|
}
|
|
222
222
|
e.preventDefault();
|
|
223
|
-
}, [viewY, viewM,
|
|
223
|
+
}, [viewY, viewM, activeDay, moveTo]);
|
|
224
224
|
const selectDay = useCallback((d) => {
|
|
225
225
|
if (disabled)
|
|
226
226
|
return; // 열린 채 disabled 가 되면 남은 달력에서 고른 날이 그대로 나갔다
|
|
@@ -239,11 +239,11 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
239
239
|
};
|
|
240
240
|
/* Tab 을 받을 날 — 활성일이 min/max 밖이라 꺼져 있으면 켜진 첫 날로. 꺼진 단추는 Tab 을 못 받아
|
|
241
241
|
격자에 Tab 자리가 **하나도 없어** 키보드로 달력에 들어갈 수 없었다(값 없음 + min=내일). */
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
245
|
-
return
|
|
246
|
-
for (let d = 1; d <=
|
|
242
|
+
const tabIndexOf = (() => {
|
|
243
|
+
const lastDay = daysInMonth(viewY, viewM);
|
|
244
|
+
if (activeDay >= 1 && activeDay <= lastDay && !isDisabledDay(activeDay))
|
|
245
|
+
return activeDay;
|
|
246
|
+
for (let d = 1; d <= lastDay; d++)
|
|
247
247
|
if (!isDisabledDay(d))
|
|
248
248
|
return d;
|
|
249
249
|
return -1;
|
|
@@ -259,15 +259,15 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
259
259
|
const dropdown = open ? createPortal(_jsxs("div", { ref: menuRef, id: panelId, className: "date-picker__dropdown", style: dropdownStyle, "data-tooltip-ignore": true, role: "dialog", "aria-label": t('datePicker.chooseDate', '날짜 선택'), tabIndex: -1, children: [_jsxs("div", { className: "date-picker__nav", children: [_jsx("button", { type: "button", className: "date-picker__nav-btn", onClick: prevMonth, "aria-label": t('calendar.prevMonth', '이전 달'), children: _jsx(ChevronLeft, { size: 16, strokeWidth: 2 }) }), _jsx(Dropdown, { className: "date-picker__nav-dropdown", ariaLabel: t('datePicker.selectYearMonth', '연월 선택'), options: ymOptions, value: ymValue, onChange: (v) => {
|
|
260
260
|
const [y, m] = v.split('-');
|
|
261
261
|
setViewYM(+y, +m - 1);
|
|
262
|
-
}, block: true, size: 40 }), _jsx("button", { type: "button", className: "date-picker__nav-btn", onClick: nextMonth, "aria-label": t('calendar.nextMonth', '다음 달'), children: _jsx(ChevronRight, { size: 16, strokeWidth: 2 }) })] }), _jsxs("div", { className: "date-picker__grid", ref:
|
|
262
|
+
}, block: true, size: 40 }), _jsx("button", { type: "button", className: "date-picker__nav-btn", onClick: nextMonth, "aria-label": t('calendar.nextMonth', '다음 달'), children: _jsx(ChevronRight, { size: 16, strokeWidth: 2 }) })] }), _jsxs("div", { className: "date-picker__grid", ref: gridRef, onKeyDown: onGridKeyDown, children: [DOW.map(d => (_jsx("div", { className: "date-picker__dow caption-1", children: d }, d))), cells.map((d, i) => {
|
|
263
263
|
if (d === null)
|
|
264
264
|
return _jsx("div", {}, `e${i}`);
|
|
265
265
|
const iso = toISO(viewY, viewM, d);
|
|
266
266
|
const isSelected = iso === value;
|
|
267
|
-
const isToday = iso ===
|
|
267
|
+
const isToday = iso === todayIso;
|
|
268
268
|
const dis = isDisabledDay(d);
|
|
269
269
|
const col = (i % 7);
|
|
270
|
-
return (_jsx("button", { type: "button", disabled: dis, "data-day": d, tabIndex: d ===
|
|
270
|
+
return (_jsx("button", { type: "button", disabled: dis, "data-day": d, tabIndex: d === tabIndexOf ? 0 : -1, "aria-current": isToday ? 'date' : undefined, onClick: () => !dis && selectDay(d), className: cx('date-picker__day caption-1', isSelected && 'date-picker__day--selected', isToday && !isSelected && 'date-picker__day--today', dis && 'date-picker__day--disabled', col === 0 && 'date-picker__day--sun', col === 6 && 'date-picker__day--sat'), children: d }, d));
|
|
271
271
|
})] })] }), document.body) : null;
|
|
272
272
|
return (_jsxs("div", { className: cx('date-picker', `date-picker--s${size}`, open && 'date-picker--open', className), children: [_jsxs("button", { ref: triggerRef, type: "button", className: "date-picker__trigger body-2", onClick: () => !disabled && toggle(), disabled: disabled, "aria-haspopup": "true", "aria-expanded": open, children: [_jsx("span", { className: value ? 'date-picker__value' : 'date-picker__placeholder', children: value ? formatDisplay(value) : placeholder }), _jsx(Calendar, { size: iconSize, className: "date-picker__icon", strokeWidth: 1.5 })] }), dropdown] }));
|
|
273
273
|
}
|
|
@@ -40,20 +40,20 @@ export default function Dialog({ open, onClose, ariaLabel, title, children, size
|
|
|
40
40
|
useEffect(() => {
|
|
41
41
|
if (!open)
|
|
42
42
|
return;
|
|
43
|
-
const
|
|
43
|
+
const returnTo = document.activeElement;
|
|
44
44
|
/* 스크롤 잠금은 **공유 카운터**(Popup·TrainingPagePopup 과 같은 것)로 — 전에는 body.style.overflow 를 제 손으로
|
|
45
45
|
기억했다 되돌려, Dialog 위에 Popup 을 열고 Dialog 를 먼저 닫으면 Popup 이 떠 있는데 스크롤이 풀리고, 이어 Popup 을
|
|
46
46
|
닫으면 overflow:hidden 으로 영영 굳었다. */
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
rememberFocusReturn(
|
|
50
|
-
|
|
47
|
+
const unlock = lockBodyScroll();
|
|
48
|
+
const surface = surfaceRef.current;
|
|
49
|
+
rememberFocusReturn(surface, returnTo);
|
|
50
|
+
surface?.focus();
|
|
51
51
|
return () => {
|
|
52
|
-
|
|
52
|
+
unlock();
|
|
53
53
|
// 포커스가 이 창 안이거나 갈 곳을 잃었을 때만 되돌린다 — 이미 다른 창으로 옮겨 갔으면 빼앗지 않는다(Popup 과 같다)
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
resolveFocusReturn(
|
|
54
|
+
const active = document.activeElement;
|
|
55
|
+
if (!active || active === document.body || (surface && surface.contains(active)))
|
|
56
|
+
resolveFocusReturn(returnTo)?.focus?.();
|
|
57
57
|
};
|
|
58
58
|
}, [open]);
|
|
59
59
|
/* ESC 는 **공유 스택**에 맡긴다 — 예전에는 window 에서 직접 들었다.
|
|
@@ -76,7 +76,7 @@ export default function Dialog({ open, onClose, ariaLabel, title, children, size
|
|
|
76
76
|
}, [open]);
|
|
77
77
|
/* 바탕 누르기 — 누른 곳과 뗀 곳이 **둘 다 바탕일 때만** 닫는다(Popup 0.128.0 과 같다). 판 안에서 글자를 긁어
|
|
78
78
|
고르다 바깥에서 놓으면 click 이 공통 조상(바탕)에 떨어져 창이 닫혔다. */
|
|
79
|
-
const
|
|
79
|
+
const pressedOnBackdrop = useRef(false);
|
|
80
80
|
/* Tab 을 이 판 안에 가둔다 — `aria-modal="true"` 는 **낭독기에게만** 통한다.
|
|
81
81
|
눈으로 보며 Tab 을 누르는 사람은 그 표시를 못 보므로, 가둠이 없으면 마지막
|
|
82
82
|
단추 다음에서 **뒤 화면**으로 빠져나간다. 실제로 그랬다: 놀이터
|
|
@@ -86,11 +86,11 @@ export default function Dialog({ open, onClose, ariaLabel, title, children, size
|
|
|
86
86
|
useFocusTrap(surfaceRef, open);
|
|
87
87
|
if (!open || typeof document === 'undefined')
|
|
88
88
|
return null;
|
|
89
|
-
return createPortal(_jsx("div", { className: cx('dialog', className), onPointerDown: closeOnBackdrop ? (e) => {
|
|
89
|
+
return createPortal(_jsx("div", { className: cx('dialog', className), onPointerDown: closeOnBackdrop ? (e) => { pressedOnBackdrop.current = e.target === e.currentTarget; } : undefined, onMouseDown: (e) => { if (e.target === e.currentTarget)
|
|
90
90
|
e.preventDefault(); }, onClick: closeOnBackdrop ? (e) => {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
if (
|
|
91
|
+
const pressed = pressedOnBackdrop.current;
|
|
92
|
+
pressedOnBackdrop.current = false;
|
|
93
|
+
if (pressed && e.target === e.currentTarget)
|
|
94
94
|
handleClose();
|
|
95
95
|
} : undefined, children: _jsxs("div", { ref: surfaceRef, role: "dialog", "aria-modal": "true", "aria-label": title ? undefined : ariaLabel, "aria-labelledby": title ? titleId : undefined, tabIndex: -1, className: cx('dialog__surface', `dialog__surface--${size}`, contentClassName), onClick: (e) => e.stopPropagation(), children: [title ? (_jsx("div", { className: "dialog__header", children: _jsx("h2", { id: titleId, className: "dialog__title h4b", children: title }) })) : null, showClose ? (_jsx("button", { type: "button", className: "dialog__close", "aria-label": t('common.close', '닫기'), onClick: handleClose, children: _jsx(CloseIcon, {}) })) : null, _jsx("div", { className: "dialog__body", children: children })] }) }), document.body);
|
|
96
96
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
import EffectPartyPopper from "./EffectPartyPopper";
|
|
5
|
-
const
|
|
5
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined);
|
|
6
6
|
/**
|
|
7
7
|
* @public 앱에 한 번 붙여 두는 폭죽 자리.
|
|
8
8
|
*
|
|
@@ -14,17 +14,17 @@ const 수 = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : u
|
|
|
14
14
|
* - 이 자리가 앱에 없으면 이벤트는 아무 일도 안 한다.
|
|
15
15
|
*/
|
|
16
16
|
export default function EffectPartyPopperHost({ trigger = 0, active = false }) {
|
|
17
|
-
const [
|
|
17
|
+
const [burst, setBurst] = useState(null);
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
if (typeof window === 'undefined')
|
|
20
20
|
return;
|
|
21
|
-
const
|
|
21
|
+
const onEvent = (e) => {
|
|
22
22
|
const d = (e.detail ?? {});
|
|
23
|
-
|
|
23
|
+
setBurst((prev) => ({ n: (prev?.n ?? 0) + 1, count: num(d.count), strength: num(d.strength), duration: num(d.duration) }));
|
|
24
24
|
};
|
|
25
|
-
window.addEventListener('party:trigger',
|
|
26
|
-
return () => window.removeEventListener('party:trigger',
|
|
25
|
+
window.addEventListener('party:trigger', onEvent);
|
|
26
|
+
return () => window.removeEventListener('party:trigger', onEvent);
|
|
27
27
|
}, []);
|
|
28
|
-
const
|
|
29
|
-
return (_jsxs(_Fragment, { children: [_jsx(EffectPartyPopper, { active: active, trigger: trigger }),
|
|
28
|
+
const clear = useCallback(() => setBurst(null), []);
|
|
29
|
+
return (_jsxs(_Fragment, { children: [_jsx(EffectPartyPopper, { active: active, trigger: trigger }), burst && (_jsx(EffectPartyPopper, { active: true, trigger: burst.n, count: burst.count, strength: burst.strength, duration: burst.duration, onComplete: clear }))] }));
|
|
30
30
|
}
|
|
@@ -28,7 +28,7 @@ export default function FilterPanel({ title, titleLevel = 2, headerAction, heade
|
|
|
28
28
|
/* 머리 아무 데나 눌러도 접힌다. 다만 **그 안의 조작부를 누른 것은 아니다** —
|
|
29
29
|
`headerAction` 의 단추(초기화 등)를 누르면 그 일만 하고 판은 그대로 둔다.
|
|
30
30
|
chevron 단추는 제 `onClick` 으로 접으므로 여기서 한 번 더 접지 않는다(두 번 접히면 제자리다). */
|
|
31
|
-
const
|
|
31
|
+
const onHeadClick = (e) => {
|
|
32
32
|
if (!collapsible)
|
|
33
33
|
return;
|
|
34
34
|
const el = e.target;
|
|
@@ -44,17 +44,17 @@ export default function FilterPanel({ title, titleLevel = 2, headerAction, heade
|
|
|
44
44
|
표 설정 판처럼 값이 아예 `WordTableBlock` 안에 있는 자리는 지을 길조차 없다(말귀 요청 ㉗).
|
|
45
45
|
`headerSummary` 를 주면 그것이 이긴다 — 글월을 부르는 쪽이 쥐던 길은 그대로 열려 있다.
|
|
46
46
|
접혀 있을 때만 쓰이므로 펼친 동안은 셈하지 않는다. */
|
|
47
|
-
const
|
|
48
|
-
const summaryNode = headerSummary ?? (
|
|
47
|
+
const defaultSummary = useMemo(() => (headerSummary !== undefined || !collapsed ? '' : buildFilterSummary(sections, selected, t('common.all', '전체'))), [headerSummary, collapsed, sections, selected, t]);
|
|
48
|
+
const summaryNode = headerSummary ?? (defaultSummary ? _jsx("span", { className: "filter-panel__header-summary body-3", children: defaultSummary }) : null);
|
|
49
49
|
const showHeader = title || headerAction || headerSummary || collapsible;
|
|
50
|
-
const
|
|
51
|
-
const
|
|
50
|
+
const titleId = useId();
|
|
51
|
+
const TitleTag = titleLevel ? `h${titleLevel}` : 'span';
|
|
52
52
|
return (
|
|
53
53
|
/* **제목이 이 판의 이름이 된다**(말귀 요청 (57), 2026-10-02). 이름 없는 판은 지역으로
|
|
54
54
|
안 잡혀, 셋이 나란히 서도 "지금 어느 판 안인지" 를 알 길이 없었다 — (51)·(54) 에서
|
|
55
55
|
`ChartSection`·`TrainingCardGroup` 에 한 것과 같은 자다.
|
|
56
56
|
**글자 제목일 때만 건다** — 마디를 가리키므로 글이 아닌 제목이면 이름이 안 선다. */
|
|
57
|
-
_jsxs("div", { className: cx('filter-panel', collapsible ? 'filter-panel--collapsible' : '', collapsed ? 'filter-panel--collapsed' : '', className), role: typeof title === 'string' && title ? 'group' : undefined, "aria-labelledby": typeof title === 'string' && title ?
|
|
57
|
+
_jsxs("div", { className: cx('filter-panel', collapsible ? 'filter-panel--collapsible' : '', collapsed ? 'filter-panel--collapsed' : '', className), role: typeof title === 'string' && title ? 'group' : undefined, "aria-labelledby": typeof title === 'string' && title ? titleId : undefined, children: [showHeader && (_jsxs("div", { className: "filter-panel__header", onClick: collapsible ? onHeadClick : undefined, children: [_jsx("div", { className: "filter-panel__title-row", children: title && _jsx(TitleTag, { id: titleId, className: "filter-panel__title h4", children: title }) }), _jsxs("div", { className: "filter-panel__header-actions", children: [collapsed ? summaryNode : headerAction, collapsible && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-down' : 'chevron-up',
|
|
58
58
|
/* Button은 aria-label을 자기 ariaLabel prop으로 덮어쓴다({...rest} 뒤에
|
|
59
59
|
aria-label={ariaLabelString}을 다시 쓴다) — kebab으로 넘기면 undefined가
|
|
60
60
|
되어 이름이 통째로 사라진다. 저장소의 다른 호출부는 모두 ariaLabel을 쓴다.
|
|
@@ -15,13 +15,13 @@ export default function FilterSectionInfo({ title, items }) {
|
|
|
15
15
|
const { anchorRect, openAt, close: handleClose } = useHoverAnchor();
|
|
16
16
|
/* 터치 — 톡하면 열렸다 떼자마자 닫혀 설명을 읽을 수 없었다. 톡으로 열고 닫는다(마우스는 그대로).
|
|
17
17
|
트리거를 품는 자리 — 상자를 만들지 않는(display: contents) 껍데기라 배치는 그대로다. */
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
openAt(
|
|
18
|
+
const anchorRef = useRef(null);
|
|
19
|
+
const touchToggle = useTouchToggle(!!anchorRect, (next, el) => { if (next && el)
|
|
20
|
+
openAt(anchorRef.current?.querySelector('button') ?? el);
|
|
21
21
|
else
|
|
22
|
-
handleClose(); },
|
|
23
|
-
return (_jsxs("span", { ref:
|
|
22
|
+
handleClose(); }, anchorRef);
|
|
23
|
+
return (_jsxs("span", { ref: anchorRef, style: { display: 'contents' }, children: [_jsx(Button, { htmlType: "button", size: 24, type: "dot", icon: "info", variant: "transparent-gray", useBorder: false, selected: !!anchorRect, ariaLabel: t('common.info', '설명'), onPointerEnter: (e) => { if (!isTouchPointer(e))
|
|
24
24
|
openAt(e.currentTarget); }, onPointerLeave: (e) => { if (!isTouchPointer(e))
|
|
25
|
-
handleClose(); }, onClick: (e) => {
|
|
25
|
+
handleClose(); }, onClick: (e) => { touchToggle.touchPress(e.currentTarget); }, onFocus: (e) => { if (!recentlyTouched())
|
|
26
26
|
openAt(e.currentTarget); }, onBlur: handleClose }), anchorRect && (_jsx(DescriptionTooltip, { title: title, items: items, showIndex: items.length > 1, anchorRect: anchorRect, onClose: handleClose }))] }));
|
|
27
27
|
}
|
|
@@ -25,5 +25,5 @@ import Button from './Button';
|
|
|
25
25
|
* @public
|
|
26
26
|
*/
|
|
27
27
|
export default function GameIntro({ icon, iconSize = 56, title, titleClass = 'h2', description, tipGroups, start, className, }) {
|
|
28
|
-
return (_jsxs("div", { className: cx('game-intro', className), children: [icon != null && (_jsx("span", { className: "game-intro__icon", "aria-hidden": "true", style: { '--game-intro-icon': `${iconSize}px` }, children: icon })), _jsx("h2", { className: cx('game-intro__title', titleClass), children: title }), description != null && _jsx("p", { className: "game-intro__description body-3", children: description }), tipGroups?.map((
|
|
28
|
+
return (_jsxs("div", { className: cx('game-intro', className), children: [icon != null && (_jsx("span", { className: "game-intro__icon", "aria-hidden": "true", style: { '--game-intro-icon': `${iconSize}px` }, children: icon })), _jsx("h2", { className: cx('game-intro__title', titleClass), children: title }), description != null && _jsx("p", { className: "game-intro__description body-3", children: description }), tipGroups?.map((group, i) => (_jsxs("div", { className: "game-intro__tips", children: [group.title != null && _jsx("span", { className: "game-intro__tips-title body-3", children: group.title }), group.tips.map((tip, j) => (_jsxs("span", { className: "game-intro__tip body-3", children: [tip.icon != null && _jsx("span", { "aria-hidden": "true", children: tip.icon }), _jsx("span", { children: tip.text })] }, j)))] }, i))), start && (_jsx(Button, { htmlType: "button", className: start.className, variant: "main", color: start.color, size: 48, width: "100%", disabled: start.disabled, onClick: start.onClick, children: start.label }))] }));
|
|
29
29
|
}
|
|
@@ -23,10 +23,10 @@ export default function Icon(props) {
|
|
|
23
23
|
const createWrapper = (children, extraProps = {}) => (_jsx("span", { className: wrapperClass, style: wrapperStyle, ...extraProps, children: children }));
|
|
24
24
|
/* 이미지(파일 경로)에도 size 를 한 변 픽셀로 준다 — 전에는 lucide·마스크만 size 를 읽어 PNG·<img> 로 그린 SVG 는
|
|
25
25
|
원본 크기로 떴다. 줄 때만 박고(object-fit: contain 으로 비율 유지), 안 주면 지금처럼 클래스·원본 크기. */
|
|
26
|
-
const
|
|
26
|
+
const imageSize = typeof size === 'number' ? { width: size, height: size, objectFit: 'contain' } : null;
|
|
27
27
|
const createImage = (src, extraProps = {}) => (
|
|
28
28
|
// 글자가 아닌 label(요소)은 alt 로 못 쓴다 — String() 하면 "[object Object]" 가 읽혔다
|
|
29
|
-
_jsx("img", { src: src, alt: typeof label === 'string' ? label : (typeof label === 'number' ? String(label) : ''), className: defaultImgClass, ...extraProps, style:
|
|
29
|
+
_jsx("img", { src: src, alt: typeof label === 'string' ? label : (typeof label === 'number' ? String(label) : ''), className: defaultImgClass, ...extraProps, style: imageSize ? { ...extraProps.style, ...imageSize } : extraProps.style }));
|
|
30
30
|
if (!icon)
|
|
31
31
|
return null;
|
|
32
32
|
if (typeof icon !== 'string')
|
|
@@ -9,12 +9,12 @@ import './ItemActions.css';
|
|
|
9
9
|
* 세 곳에 베껴져 있을 때 실제로 갈라진 것이 이 표의 값들이었다 — 링크 단추의 조건이
|
|
10
10
|
* 둘은 `&&`, 하나는 늘 세움이었다. 한자리에 있으면 갈라질 자리가 없다.
|
|
11
11
|
*/
|
|
12
|
-
const
|
|
13
|
-
{
|
|
14
|
-
{
|
|
15
|
-
{
|
|
16
|
-
{
|
|
17
|
-
{
|
|
12
|
+
const order = [
|
|
13
|
+
{ key: 'edit', icon: 'edit-2', closeFirst: true },
|
|
14
|
+
{ key: 'duplicate', icon: 'copy', closeFirst: false },
|
|
15
|
+
{ key: 'publish', icon: 'book-marked', closeFirst: false },
|
|
16
|
+
{ key: 'copyLink', icon: 'link', closeFirst: false },
|
|
17
|
+
{ key: 'remove', icon: 'trash-2', closeFirst: true },
|
|
18
18
|
];
|
|
19
19
|
/**
|
|
20
20
|
* **보고 있는 한 항목에 하는 일들** — 창 바닥 왼쪽에 서는 아이콘 단추 줄.
|
|
@@ -67,16 +67,16 @@ const 차례 = [
|
|
|
67
67
|
*/
|
|
68
68
|
export default function ItemActions(props) {
|
|
69
69
|
const { onClose, className } = props;
|
|
70
|
-
return (_jsx("div", { className: cx('item-actions', className), children:
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
70
|
+
return (_jsx("div", { className: cx('item-actions', className), children: order.map(({ key, icon, closeFirst }) => {
|
|
71
|
+
const given = props[key];
|
|
72
|
+
if (!given)
|
|
73
73
|
return null;
|
|
74
|
-
const
|
|
75
|
-
? (
|
|
76
|
-
?
|
|
77
|
-
:
|
|
78
|
-
:
|
|
79
|
-
return (_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: icon, ariaLabel:
|
|
80
|
-
onClose();
|
|
74
|
+
const action = key === 'publish'
|
|
75
|
+
? (given.published
|
|
76
|
+
? given.unpublish
|
|
77
|
+
: given.publish)
|
|
78
|
+
: given;
|
|
79
|
+
return (_jsx(Button, { htmlType: "button", size: 40, type: "dot", icon: icon, ariaLabel: action.label, showTooltipInfo: action.label, onClick: () => { if (closeFirst)
|
|
80
|
+
onClose(); action.onClick(); } }, key));
|
|
81
81
|
}) }));
|
|
82
82
|
}
|
|
@@ -34,21 +34,21 @@ export default function LandingHeader({ logo, logoAlt = '', logoHref, links = []
|
|
|
34
34
|
if (!open)
|
|
35
35
|
return;
|
|
36
36
|
// 서랍 안에 포커스가 있으면 햄버거로 돌려 준다 — 서랍이 hidden 이 되며 포커스가 숨은 링크에 남아 갈 곳을 잃었다
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
37
|
+
const popEsc = pushEsc(() => {
|
|
38
|
+
const drawer = document.getElementById(drawerId);
|
|
39
|
+
if (drawer && drawer.contains(document.activeElement))
|
|
40
40
|
toggleRef.current?.focus();
|
|
41
41
|
setOpen(false);
|
|
42
42
|
});
|
|
43
43
|
const mq = typeof window !== 'undefined' && window.matchMedia ? window.matchMedia('(min-width: 961px)') : null;
|
|
44
|
-
const
|
|
44
|
+
const onWiden = (e) => { if (e.matches)
|
|
45
45
|
setOpen(false); };
|
|
46
46
|
if (mq) {
|
|
47
47
|
if (mq.matches)
|
|
48
48
|
setOpen(false);
|
|
49
|
-
mq.addEventListener?.('change',
|
|
49
|
+
mq.addEventListener?.('change', onWiden);
|
|
50
50
|
}
|
|
51
|
-
return () => {
|
|
51
|
+
return () => { popEsc(); mq?.removeEventListener?.('change', onWiden); };
|
|
52
52
|
}, [open, drawerId]);
|
|
53
53
|
const innerClass = containerClassName
|
|
54
54
|
? `${containerClassName} landing-header__inner`
|
|
@@ -7,7 +7,7 @@ import Tooltip from './Tooltip/Tooltip';
|
|
|
7
7
|
import { useDelayedHoverAnchor } from '../hooks/useHoverAnchor';
|
|
8
8
|
/** 점 위에 잠깐 머물면 뜨는 뜸 — 지나가는 마우스에 툴팁이 번쩍이지 않을 만큼만 짧게 @public */
|
|
9
9
|
export const LINE_CHART_HOVER_DELAY_MS = 120;
|
|
10
|
-
const
|
|
10
|
+
const defaultInsets = { left: 38, right: 16, top: 26, bottom: 24 };
|
|
11
11
|
/**
|
|
12
12
|
* **작은 선 그래프** — 시간·조건에 따라 값이 어떻게 흐르는지를 그린다.
|
|
13
13
|
* 세로는 0~100%로 고정이고, 가로 범위는 부르는 쪽이 준다.
|
|
@@ -24,36 +24,36 @@ const 기본여백 = { left: 38, right: 16, top: 26, bottom: 24 };
|
|
|
24
24
|
export default function LineChart({ points = [], range, ticks = [], axisTitle, color, height = 156, fallbackWidth = 320, insets, overlay, ariaLabel, className, }) {
|
|
25
25
|
/* **제 상자를 재어 그 픽셀 수를 그림 칸으로 쓴다.** `width: 100%` 는 그대로라 viewBox 를 바꿔도
|
|
26
26
|
가로가 다시 바뀌지 않는다(되먹임이 없다). */
|
|
27
|
-
const [
|
|
28
|
-
const [
|
|
27
|
+
const [measuredWidth, setMeasuredWidth] = useState(null);
|
|
28
|
+
const [svgEl, setSvgEl] = useState(null);
|
|
29
29
|
useEffect(() => {
|
|
30
|
-
if (
|
|
30
|
+
if (!svgEl)
|
|
31
31
|
return;
|
|
32
|
-
const
|
|
33
|
-
|
|
32
|
+
const measure = (w) => setMeasuredWidth(Math.round(w) || null);
|
|
33
|
+
measure(svgEl.getBoundingClientRect().width);
|
|
34
34
|
if (typeof ResizeObserver === 'undefined')
|
|
35
35
|
return;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
return () => {
|
|
39
|
-
}, [
|
|
40
|
-
const { payload:
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
openAfterDelay(
|
|
36
|
+
const observer = new ResizeObserver(([e]) => measure(e.contentRect.width));
|
|
37
|
+
observer.observe(svgEl);
|
|
38
|
+
return () => { observer.disconnect(); };
|
|
39
|
+
}, [svgEl]);
|
|
40
|
+
const { payload: hoverText, anchorRect: hoverRect, openAfterDelay, close: close } = useDelayedHoverAnchor(LINE_CHART_HOVER_DELAY_MS);
|
|
41
|
+
const onHover = useCallback((text, el) => {
|
|
42
|
+
if (text)
|
|
43
|
+
openAfterDelay(text, el.getBoundingClientRect());
|
|
44
44
|
}, [openAfterDelay]);
|
|
45
|
-
const W =
|
|
46
|
-
const { left:
|
|
47
|
-
const [
|
|
45
|
+
const W = measuredWidth ?? fallbackWidth, H = height;
|
|
46
|
+
const { left: left, right: right, top: top, bottom: bottom } = { ...defaultInsets, ...insets };
|
|
47
|
+
const [first, last] = range;
|
|
48
48
|
/* 범위는 부르는 쪽이 적어도 한 칸 폭으로 준다(끝 > 처음) — 같으면 0 으로 나누므로 왼쪽 끝에 몰아 둔다 */
|
|
49
|
-
const x = (v) => (
|
|
50
|
-
const y = (pct) =>
|
|
51
|
-
const
|
|
49
|
+
const x = (v) => (last === first ? left : left + ((W - left - right) * (v - first)) / (last - first));
|
|
50
|
+
const y = (pct) => top + ((H - top - bottom) * (100 - pct)) / 100;
|
|
51
|
+
const scales = {
|
|
52
52
|
x, y,
|
|
53
|
-
plot: { left:
|
|
53
|
+
plot: { left: left, right: W - right, top: top, bottom: H - bottom, width: Math.max(0, W - left - right), height: Math.max(0, H - top - bottom) },
|
|
54
54
|
};
|
|
55
55
|
/* 선은 **믿을 점만** 잇는다 — 속 빈 점까지 이으면 "이만큼 봤다" 는 말이 흐려진다 */
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
return (_jsxs(_Fragment, { children: [_jsxs("svg", { ref:
|
|
56
|
+
const linePoints = points.filter(p => !p.hollow);
|
|
57
|
+
const strokeColor = color ?? 'var(--color-main)';
|
|
58
|
+
return (_jsxs(_Fragment, { children: [_jsxs("svg", { ref: setSvgEl, className: cx('line-chart', className), viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": ariaLabel ?? points.map(p => p.description).filter(Boolean).join(', '), children: [axisTitle && _jsx("text", { className: "line-chart__axis-title", x: 0, y: 12, children: axisTitle }), [0, 100].map(v => (_jsxs("g", { children: [_jsx("line", { className: "line-chart__grid", x1: left, x2: W - right, y1: y(v), y2: y(v) }), _jsxs("text", { className: "line-chart__tick", x: left - 6, y: y(v) + 4, textAnchor: "end", children: [v, "%"] })] }, v))), overlay?.(scales), ticks.map(tk => (_jsx("text", { className: "line-chart__tick", x: x(tk.x), y: H - 5, textAnchor: "middle", children: tk.label }, tk.x))), linePoints.length > 1 && (_jsx("polyline", { className: "line-chart__line", fill: "none", stroke: strokeColor, points: linePoints.map(p => `${x(p.x)},${y(p.pct)}`).join(' ') })), points.map(p => (_jsxs("g", { className: "line-chart__dot", onPointerEnter: e => onHover(p.description, e.currentTarget), onPointerLeave: close, children: [_jsx("circle", { className: "line-chart__dot-hit", cx: x(p.x), cy: y(p.pct), r: 12 }), _jsx("circle", { className: cx('line-chart__dot-main', p.hollow && 'is-hollow'), cx: x(p.x), cy: y(p.pct), r: 4, stroke: strokeColor, fill: p.hollow ? undefined : strokeColor })] }, p.x)))] }), hoverText != null && (_jsx(Tooltip, { anchorRect: hoverRect, offset: 10, closeOnOutsideClick: false, className: "line-chart__tip", role: "tooltip", ariaLabel: hoverText, children: hoverText }))] }));
|
|
59
59
|
}
|