@ihabdevteam/core 0.309.0 → 0.311.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/components.cjs.js +15 -15
  3. package/dist/components.esm.css +2 -2
  4. package/dist/hooks.cjs.js +3 -3
  5. package/dist/i18n.cjs.js +3 -3
  6. package/dist/ihabdevteam-core.cjs.js +185 -185
  7. package/dist/ihabdevteam-core.d.ts +120 -10
  8. package/dist/packages/core/src/components/CountTile.css +4 -0
  9. package/dist/packages/core/src/components/CountTile.d.ts +11 -1
  10. package/dist/packages/core/src/components/CountTile.js +2 -2
  11. package/dist/packages/core/src/components/Popup/Popup.css +12 -0
  12. package/dist/packages/core/src/components/Popup/Popup.d.ts +11 -0
  13. package/dist/packages/core/src/components/Popup/Popup.js +4 -2
  14. package/dist/packages/core/src/components/Tooltip/TrainingTooltip.d.ts +31 -3
  15. package/dist/packages/core/src/components/Tooltip/TrainingTooltip.js +25 -13
  16. package/dist/packages/core/src/components/Training/TrainingBlock.css +18 -0
  17. package/dist/packages/core/src/components/Training/TrainingBlockGrid.d.ts +27 -0
  18. package/dist/packages/core/src/components/Training/TrainingBlockGrid.js +65 -0
  19. package/dist/packages/core/src/components/Training/TrainingStep.d.ts +10 -2
  20. package/dist/packages/core/src/components/Training/TrainingStep.js +2 -2
  21. package/dist/packages/core/src/components/Training/TrainingStepCard.d.ts +4 -1
  22. package/dist/packages/core/src/components/Training/TrainingStepCard.js +2 -2
  23. package/dist/packages/core/src/components/Training/TrainingStepCardOrder.d.ts +4 -1
  24. package/dist/packages/core/src/components/Training/TrainingStepCardOrder.js +2 -2
  25. package/dist/packages/core/src/components/Training/TrainingStepConfirm.d.ts +4 -1
  26. package/dist/packages/core/src/components/Training/TrainingStepConfirm.js +2 -2
  27. package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.d.ts +4 -1
  28. package/dist/packages/core/src/components/Training/TrainingStepConfirmWithText.js +2 -2
  29. package/dist/packages/core/src/components/Training/TrainingStepOrderList.d.ts +1 -1
  30. package/dist/packages/core/src/components/Training/TrainingStepOrderList.js +2 -2
  31. package/dist/packages/core/src/components/Training/trainingStepOrderListTypes.d.ts +3 -0
  32. package/dist/packages/core/src/components/Training/trainingStepOrderListTypes.js +0 -4
  33. package/dist/packages/core/src/components.d.ts +3 -1
  34. package/dist/packages/core/src/components.js +1 -0
  35. package/dist/packages/core/src/i18n/locales/en.json +0 -1
  36. package/dist/packages/core/src/i18n/locales/ko.json +0 -1
  37. package/dist/packages/core/src/utils/blockGrid.d.ts +16 -0
  38. package/dist/packages/core/src/utils/blockGrid.js +28 -0
  39. package/dist/packages/core/src/utils.d.ts +1 -0
  40. package/dist/packages/core/src/utils.js +1 -0
  41. package/dist/packages/core/src/version.d.ts +1 -1
  42. package/dist/packages/core/src/version.js +1 -1
  43. package/dist/utils.cjs.js +1 -1
  44. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from 'react';
1
2
  import './TrainingStep.css';
2
3
  import './TrainingStepCard.css';
3
4
  /** @public */
@@ -17,6 +18,8 @@ export type TrainingStepCardProps = {
17
18
  index?: number;
18
19
  stepIndex?: number;
19
20
  isLast?: boolean;
21
+ /** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
22
+ lockedHint?: ReactNode | null;
20
23
  title?: string;
21
24
  onDone?: (payload?: unknown) => void;
22
25
  onChange?: (payload?: unknown) => void;
@@ -49,4 +52,4 @@ export type TrainingStepCardProps = {
49
52
  playAllGapMs?: number;
50
53
  };
51
54
  /** @public */
52
- export default function TrainingStepCard({ index, stepIndex, isLast, title, onDone, onChange, context, selectable, hideLabel, hideAudio, maxCards, playAllGapMs }: TrainingStepCardProps): import("react").JSX.Element;
55
+ export default function TrainingStepCard({ index, stepIndex, isLast, lockedHint, title, onDone, onChange, context, selectable, hideLabel, hideAudio, maxCards, playAllGapMs }: TrainingStepCardProps): import("react").JSX.Element;
@@ -21,7 +21,7 @@ function withTimeout(promise, ms) {
21
21
  });
22
22
  }
23
23
  /** @public */
24
- export default function TrainingStepCard({ index, stepIndex, isLast = false, title, onDone, onChange, context = [], selectable = false, hideLabel = false, hideAudio = false, maxCards, playAllGapMs = 0 }) {
24
+ export default function TrainingStepCard({ index, stepIndex, isLast = false, lockedHint, title, onDone, onChange, context = [], selectable = false, hideLabel = false, hideAudio = false, maxCards, playAllGapMs = 0 }) {
25
25
  const { t } = useTranslation();
26
26
  const resolvedTitle = title ?? t('training.cardInstructions', '낱말 카드를 보고 소리를 듣고 확인하세요.');
27
27
  const items = useMemo(() => Array.isArray(context) ? context : (context && context.items) || [], [context]);
@@ -133,7 +133,7 @@ export default function TrainingStepCard({ index, stepIndex, isLast = false, tit
133
133
  catch (e) { }
134
134
  }
135
135
  }, [onChange, currentState, onDone]);
136
- return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, onDone: onDone, titleProps: {
136
+ return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, lockedHint: lockedHint, title: resolvedTitle, onDone: onDone, titleProps: {
137
137
  usePlayAllBtn: !hideAudio && shown.length > 0,
138
138
  onPlayAll: handlePlayAll,
139
139
  playAllActive: playingAll,
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from 'react';
1
2
  import './TrainingStep.css';
2
3
  import './TrainingStepCard.css';
3
4
  /** @public */
@@ -5,6 +6,8 @@ export interface TrainingStepCardOrderProps {
5
6
  index?: number;
6
7
  stepIndex?: number;
7
8
  isLast?: boolean;
9
+ /** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
10
+ lockedHint?: ReactNode | null;
8
11
  title?: string;
9
12
  /** 카드에 표시할 단어 목록 (섞인 순서) */
10
13
  choices?: string[];
@@ -12,4 +15,4 @@ export interface TrainingStepCardOrderProps {
12
15
  onChange?: (payload?: unknown) => void;
13
16
  }
14
17
  /** @public */
15
- export default function TrainingStepCardOrder({ index, stepIndex, isLast, title, choices, onDone, onChange, }: TrainingStepCardOrderProps): import("react").JSX.Element;
18
+ export default function TrainingStepCardOrder({ index, stepIndex, isLast, lockedHint, title, choices, onDone, onChange, }: TrainingStepCardOrderProps): import("react").JSX.Element;
@@ -7,7 +7,7 @@ import TrainingWordCard from './TrainingWordCard';
7
7
  import './TrainingStep.css';
8
8
  import './TrainingStepCard.css';
9
9
  /** @public */
10
- export default function TrainingStepCardOrder({ index, stepIndex, isLast = false, title, choices = [], onDone, onChange, }) {
10
+ export default function TrainingStepCardOrder({ index, stepIndex, isLast = false, lockedHint, title, choices = [], onDone, onChange, }) {
11
11
  const { t } = useTranslation();
12
12
  const idx = typeof index === 'number' ? index : -1;
13
13
  const currentState = typeof stepIndex !== 'number'
@@ -74,5 +74,5 @@ export default function TrainingStepCardOrder({ index, stepIndex, isLast = false
74
74
  }
75
75
  }, [currentState, selectedOrder, nextRank, onChange, onDone, choices]);
76
76
  const resolvedTitle = title ?? t('training.selectInOrder', '들은 순서대로 카드를 선택하세요.');
77
- return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, onDone: onDone, children: _jsx("div", { className: "training-step__cards", children: choices.map((word, i) => (_jsx(TrainingWordCard, { item: { word }, disabled: currentState === 'disabled', selectable: true, selected: selectedOrder[i] !== null, orderRank: selectedOrder[i], onSelect: () => handleSelect(i), flipDelay: i * 120, hideLabel: false, hideAudio: true }, `${word}-${i}`))) }) }));
77
+ return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, lockedHint: lockedHint, title: resolvedTitle, onDone: onDone, children: _jsx("div", { className: "training-step__cards", children: choices.map((word, i) => (_jsx(TrainingWordCard, { item: { word }, disabled: currentState === 'disabled', selectable: true, selected: selectedOrder[i] !== null, orderRank: selectedOrder[i], onSelect: () => handleSelect(i), flipDelay: i * 120, hideLabel: false, hideAudio: true }, `${word}-${i}`))) }) }));
78
78
  }
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from 'react';
1
2
  import "./TrainingStep.css";
2
3
  import type { TrainingPayload } from '../../types/training';
3
4
  /** @public */
@@ -5,6 +6,8 @@ export type TrainingStepConfirmProps = {
5
6
  index?: number;
6
7
  stepIndex?: number;
7
8
  isLast?: boolean;
9
+ /** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
10
+ lockedHint?: ReactNode | null;
8
11
  title?: string;
9
12
  useTitleFaded?: boolean;
10
13
  onDone?: (payload?: TrainingPayload) => void;
@@ -31,4 +34,4 @@ export type TrainingStepConfirmProps = {
31
34
  shuffleChoices?: boolean;
32
35
  };
33
36
  /** @public */
34
- export default function TrainingStepConfirm({ index, stepIndex, isLast, title, useTitleFaded, onDone, onChange, answer, wrongAnswers, choices: propChoices, multiSelect, correctCount, useDisabledCovered, choiceIcons, shuffleChoices }: TrainingStepConfirmProps): import("react").JSX.Element;
37
+ export default function TrainingStepConfirm({ index, stepIndex, isLast, lockedHint, title, useTitleFaded, onDone, onChange, answer, wrongAnswers, choices: propChoices, multiSelect, correctCount, useDisabledCovered, choiceIcons, shuffleChoices }: TrainingStepConfirmProps): import("react").JSX.Element;
@@ -7,7 +7,7 @@ import "./TrainingStep.css";
7
7
  import TrainingStep from "./TrainingStep";
8
8
  import { shuffle as shuffleArray } from '../../utils/array';
9
9
  /** @public */
10
- export default function TrainingStepConfirm({ index, stepIndex, isLast = false, title, useTitleFaded, onDone, onChange, answer, wrongAnswers, choices: propChoices, multiSelect = false, correctCount = 0, useDisabledCovered = false, choiceIcons, shuffleChoices = true }) {
10
+ export default function TrainingStepConfirm({ index, stepIndex, isLast = false, lockedHint, title, useTitleFaded, onDone, onChange, answer, wrongAnswers, choices: propChoices, multiSelect = false, correctCount = 0, useDisabledCovered = false, choiceIcons, shuffleChoices = true }) {
11
11
  const { t } = useTranslation();
12
12
  const resolvedTitle = title ?? t('training.selectConfirm', '문제를 보고 해당하는 내용을 선택하세요.');
13
13
  const idx = typeof index === 'number' ? index : -1;
@@ -123,5 +123,5 @@ export default function TrainingStepConfirm({ index, stepIndex, isLast = false,
123
123
  때문이다 — 나머지 둘(choices 바뀜·페이지 전환)은 빈 Set 으로 되돌리는 자리라
124
124
  저 effect 의 `size === 0` 가드에 걸려 어차피 아무것도 안 했다. */
125
125
  const btnsClassName = `training-step__buttons${Array.isArray(choices) && choices.length > 3 ? ' training-step__buttons--wrap-3' : ''}`;
126
- return (_jsxs(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, useTitleFaded: useTitleFaded, children: [_jsx("div", { className: btnsClassName, children: Array.isArray(choices) && choices.length > 0 ? (choices.map((c, i) => (_jsx(Button, { htmlType: "button", size: 56, type: "bar", onClick: () => handleSelect(i), ariaLabel: String(c), label: c, useLabel: true, icon: choiceIcons ? choiceIcons[String(c)] : undefined, variant: 'default', disabled: currentState === 'disabled', selected: multiSelect ? selectedSet.has(c) : selectedAnswer === c, useDisabledCovered: useDisabledCovered, minWidth: 0 }, String(c))))) : null }), multiSelect && (_jsx("div", { className: "training-step__buttons", children: _jsx(Button, { htmlType: "button", size: 56, type: "bar", variant: "main", onClick: handleMultiSubmit, disabled: currentState !== 'active' || selectedSet.size === 0, ariaLabel: t('common.confirm', '확인'), label: t('common.confirm', '확인'), useLabel: true, minWidth: 0 }) }))] }));
126
+ return (_jsxs(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, lockedHint: lockedHint, title: resolvedTitle, useTitleFaded: useTitleFaded, children: [_jsx("div", { className: btnsClassName, children: Array.isArray(choices) && choices.length > 0 ? (choices.map((c, i) => (_jsx(Button, { htmlType: "button", size: 56, type: "bar", onClick: () => handleSelect(i), ariaLabel: String(c), label: c, useLabel: true, icon: choiceIcons ? choiceIcons[String(c)] : undefined, variant: 'default', disabled: currentState === 'disabled', selected: multiSelect ? selectedSet.has(c) : selectedAnswer === c, useDisabledCovered: useDisabledCovered, minWidth: 0 }, String(c))))) : null }), multiSelect && (_jsx("div", { className: "training-step__buttons", children: _jsx(Button, { htmlType: "button", size: 56, type: "bar", variant: "main", onClick: handleMultiSubmit, disabled: currentState !== 'active' || selectedSet.size === 0, ariaLabel: t('common.confirm', '확인'), label: t('common.confirm', '확인'), useLabel: true, minWidth: 0 }) }))] }));
127
127
  }
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from 'react';
1
2
  import "./TrainingStep.css";
2
3
  import type { TrainingPayload } from '../../types/training';
3
4
  /** @public */
@@ -5,6 +6,8 @@ export type TrainingStepConfirmWithTextProps = {
5
6
  index?: number;
6
7
  stepIndex?: number;
7
8
  isLast?: boolean;
9
+ /** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
10
+ lockedHint?: ReactNode | null;
8
11
  title?: string;
9
12
  onDone?: (payload?: TrainingPayload) => void;
10
13
  onChange?: (payload?: TrainingPayload) => void;
@@ -13,4 +16,4 @@ export type TrainingStepConfirmWithTextProps = {
13
16
  demoSelected?: number | boolean | null;
14
17
  };
15
18
  /** @public */
16
- export default function TrainingStepConfirmWithText({ index, stepIndex, isLast, title, onDone, onChange, context, demoSelected }: TrainingStepConfirmWithTextProps): import("react").JSX.Element;
19
+ export default function TrainingStepConfirmWithText({ index, stepIndex, isLast, lockedHint, title, onDone, onChange, context, demoSelected }: TrainingStepConfirmWithTextProps): import("react").JSX.Element;
@@ -7,7 +7,7 @@ import "./TrainingStep.css";
7
7
  import TrainingStep from "./TrainingStep";
8
8
  import TrainingStepTextBox from "./TrainingStepTextBox";
9
9
  /** @public */
10
- export default function TrainingStepConfirmWithText({ index, stepIndex, isLast = false, title, onDone, onChange, context, demoSelected }) {
10
+ export default function TrainingStepConfirmWithText({ index, stepIndex, isLast = false, lockedHint, title, onDone, onChange, context, demoSelected }) {
11
11
  const { t } = useTranslation();
12
12
  const resolvedTitle = title ?? t('training.confirmTitle', '문장을 확인하고 잘 들렸는지 선택하세요.');
13
13
  const idx = typeof index === 'number' ? index : -1;
@@ -70,7 +70,7 @@ export default function TrainingStepConfirmWithText({ index, stepIndex, isLast =
70
70
  setSelectedAnswer(true);
71
71
  handleEndedLocal(true);
72
72
  }, [handleEndedLocal, currentState]);
73
- return (_jsxs(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, onDone: onDone, useTitleFaded: false, children: [_jsx(TrainingStepTextBox, { context: context, state: currentState }), _jsx("div", { className: "training-step__buttons", children: Array.isArray(choices) && choices.length > 0 ? (choices.slice(0, 6).map((c, i) => (_jsx(Button, { htmlType: "button", size: 56, type: "bar", onClick: () => {
73
+ return (_jsxs(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, lockedHint: lockedHint, title: resolvedTitle, onDone: onDone, useTitleFaded: false, children: [_jsx(TrainingStepTextBox, { context: context, state: currentState }), _jsx("div", { className: "training-step__buttons", children: Array.isArray(choices) && choices.length > 0 ? (choices.slice(0, 6).map((c, i) => (_jsx(Button, { htmlType: "button", size: 56, type: "bar", onClick: () => {
74
74
  if (currentState !== 'active')
75
75
  return;
76
76
  setSelectedAnswer(i);
@@ -3,4 +3,4 @@ import "./TrainingStep.css";
3
3
  import "./TrainingStepOrderList.css";
4
4
  import type { TrainingStepOrderListProps } from './trainingStepOrderListTypes';
5
5
  export type { TrainingContextItem, ShuffledItem, ChangeResult, TrainingStepOrderListProps } from './trainingStepOrderListTypes';
6
- export default function TrainingStepOrderList({ index, stepIndex, isLast, title, onDone, onChange, context, }: TrainingStepOrderListProps): import("react").JSX.Element;
6
+ export default function TrainingStepOrderList({ index, stepIndex, isLast, lockedHint, title, onDone, onChange, context, }: TrainingStepOrderListProps): import("react").JSX.Element;
@@ -12,7 +12,7 @@ import "./TrainingStepOrderList.css";
12
12
  /* 기본 context 는 **모듈에 하나** — 매 렌더 새 `[]` 면 [context] effect 가 매번 다시 돌아 setState·onChange 로 렌더가 끝없이
13
13
  반복됐다(자료를 불러오기 전 context 없이 그린 단계에서 CPU 100%). */
14
14
  const 빈문맥 = [];
15
- export default function TrainingStepOrderList({ index, stepIndex, isLast = false, title, onDone, onChange, context = 빈문맥, }) {
15
+ export default function TrainingStepOrderList({ index, stepIndex, isLast = false, lockedHint, title, onDone, onChange, context = 빈문맥, }) {
16
16
  const { t } = useTranslation();
17
17
  const resolvedTitle = title ?? t('training.orderListDefault', '문장을 들은 순서대로 배치하세요.');
18
18
  const toText = (v) => typeof v === "string"
@@ -459,7 +459,7 @@ export default function TrainingStepOrderList({ index, stepIndex, isLast = false
459
459
  }
460
460
  };
461
461
  }, []);
462
- return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, title: resolvedTitle, children: _jsx("div", { className: "order-list", children: shuffled.map((it, idx) => {
462
+ return (_jsx(TrainingStep, { index: index, stepIndex: stepIndex, isLast: isLast, lockedHint: lockedHint, title: resolvedTitle, children: _jsx("div", { className: "order-list", children: shuffled.map((it, idx) => {
463
463
  const total = Math.max(1, (shuffled || []).length);
464
464
  const pct = total > 1 ? Math.round((idx / (total - 1)) * 100) : 50;
465
465
  return (_jsx("div", { className: `order-list__item`, children: _jsxs("div", { className: "order-list__slot", "data-disabled": currentState === 'disabled', "aria-disabled": currentState === 'disabled', children: [currentState !== 'disabled' ? (_jsx("div", { className: "order-list__marker caption-1", style: { '--marker-pos': pct + '%' }, ref: (el) => {
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from 'react';
1
2
  /**
2
3
  * trainingStepOrderListTypes.ts
3
4
  * TrainingStepOrderList의 순수 타입 정의.
@@ -27,6 +28,8 @@ export interface TrainingStepOrderListProps {
27
28
  index?: number;
28
29
  stepIndex?: number;
29
30
  isLast?: boolean;
31
+ /** 잠긴 까닭 한 줄 — `TrainingStep` 으로 그대로 넘긴다. (107): 안 주면 안 선다. (99) 때는 이 겉싸개가 안 넘겨 맨 `TrainingStep` 에서만 줄 수 있었다 */
32
+ lockedHint?: ReactNode | null;
30
33
  title?: string;
31
34
  onDone?: (res: ChangeResult) => void;
32
35
  onChange?: (res: ChangeResult) => void;
@@ -1,5 +1 @@
1
- /**
2
- * trainingStepOrderListTypes.ts
3
- * TrainingStepOrderList의 순수 타입 정의.
4
- */
5
1
  export {};
@@ -170,6 +170,8 @@ export { default as DeviceSideTable } from './components/Popup/DeviceSideTable';
170
170
  export type { DeviceRow, DeviceSideTableProps } from './components/Popup/DeviceSideTable';
171
171
  export { default as Calendar } from './components/Calendar';
172
172
  export { default as TrainingBlock } from './components/Training/TrainingBlock';
173
+ export { default as TrainingBlockGrid } from './components/Training/TrainingBlockGrid';
174
+ export type { TrainingBlockGridProps } from './components/Training/TrainingBlockGrid';
173
175
  export { default as TrainingBlockButton } from './components/Training/TrainingBlockButton';
174
176
  export { default as TrainingWordCard } from './components/Training/TrainingWordCard';
175
177
  export type { TrainingWordCardProps, TrainingWordCardItem, TrainingWordCardHandle } from './components/Training/TrainingWordCard';
@@ -249,7 +251,7 @@ export { default as DescriptionTooltip } from './components/Tooltip/DescriptionT
249
251
  export { default as TextBoxTooltip } from './components/Tooltip/TextBoxTooltip';
250
252
  export { default as TooltipTextBox } from './components/Tooltip/TextBoxTooltip';
251
253
  export { default as TrainingTooltip, TRAINING_TOOLTIP_HOVER_DELAY_MS } from './components/Tooltip/TrainingTooltip';
252
- export type { TooltipTrainingProps } from './components/Tooltip/TrainingTooltip';
254
+ export type { TooltipTrainingProps, TrainingTooltipGroup } from './components/Tooltip/TrainingTooltip';
253
255
  export { default as EffectPartyPopper } from './components/Effect/EffectPartyPopper';
254
256
  export { default as EffectPartyPopperHost } from './components/Effect/EffectPartyPopperHost';
255
257
  export * from './CoreScope';
@@ -109,6 +109,7 @@ export { default as Table } from './components/Table';
109
109
  export { default as DeviceSideTable } from './components/Popup/DeviceSideTable';
110
110
  export { default as Calendar } from './components/Calendar';
111
111
  export { default as TrainingBlock } from './components/Training/TrainingBlock';
112
+ export { default as TrainingBlockGrid } from './components/Training/TrainingBlockGrid';
112
113
  export { default as TrainingBlockButton } from './components/Training/TrainingBlockButton';
113
114
  export { default as TrainingWordCard } from './components/Training/TrainingWordCard';
114
115
  export { default as WordTablePopup } from './components/Training/WordTablePopup';
@@ -375,7 +375,6 @@
375
375
  "trainingSettingsTitle": "Training Settings",
376
376
  "sentencePreview": "Sentence Preview",
377
377
  "notHeard": "Didn't hear it.",
378
- "answerLockedHint": "You can choose once it finishes playing.",
379
378
  "heardWell": "Heard it well.",
380
379
  "confirmTitle": "Check the sentence and select whether you heard it.",
381
380
  "noSoundWaveform": "Waveform is not shown for sound-comparison questions.",
@@ -373,7 +373,6 @@
373
373
  "trainingSettingsTitle": "훈련 설정",
374
374
  "sentencePreview": "문장 미리보기",
375
375
  "notHeard": "안 들렸어요.",
376
- "answerLockedHint": "다 들으면 고를 수 있어요.",
377
376
  "heardWell": "잘 들렸어요.",
378
377
  "confirmTitle": "문장을 확인하고 잘 들렸는지 선택하세요.",
379
378
  "noSoundWaveform": "소리 비교 문제는 웨이브폼을 표현하지 않습니다.",
@@ -0,0 +1,16 @@
1
+ /**
2
+ * **실행 창의 카드 줄 — 접힐 때만 고르게**(말귀 요청 (110), 사용자 지시 2026-10-10
3
+ * *"오버라이드하거나 반복적으로 주입하는 건 코어에 요청해서 공통으로"*).
4
+ *
5
+ * 정사각 카드 다섯이 넷+하나로 접혀 마지막 한 장이 빈 판 가운데 떠 보였다(사용자 보고). 앱이 창 최대
6
+ * 1140 · 좌우 40 · 안쪽 90(실측) · 카드 200/190 을 **베껴 들고** 칸 수를 세어 `--run-cols` 로 주입했다 —
7
+ * 코어가 하나라도 바꾸면 어긋난다. 여기서는 **아무 치수도 안 벤다**: 그리드가 제 폭과 첫 카드의 폭을
8
+ * 재서 센다(`TrainingBlockGrid`). 순수 셈만 여기 둔다.
9
+ *
10
+ * 규칙(사용자: *"무조건이 아니라 개행될 때만"*): 한 줄에 다 들면 그대로, 안 들어 접히면 줄 수를
11
+ * 먼저 정하고(`ceil(n / 한줄)`) 고르게 나눈다 — 한 줄 넷이면 5 → 3+2 · 6 → 3+3 · 7 → 4+3.
12
+ */
13
+ /** 이 폭에 카드가 한 줄에 몇 장 드나(최소 1) */
14
+ export declare function columnsFitting(width: number, cardWidth: number, gap: number): number;
15
+ /** 장수와 한 줄 최대로 칸 수 — 한 줄에 다 들면 그대로, 접히면 고르게 */
16
+ export declare function balancedColumns(count: number, perRow: number): number;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * **실행 창의 카드 줄 — 접힐 때만 고르게**(말귀 요청 (110), 사용자 지시 2026-10-10
3
+ * *"오버라이드하거나 반복적으로 주입하는 건 코어에 요청해서 공통으로"*).
4
+ *
5
+ * 정사각 카드 다섯이 넷+하나로 접혀 마지막 한 장이 빈 판 가운데 떠 보였다(사용자 보고). 앱이 창 최대
6
+ * 1140 · 좌우 40 · 안쪽 90(실측) · 카드 200/190 을 **베껴 들고** 칸 수를 세어 `--run-cols` 로 주입했다 —
7
+ * 코어가 하나라도 바꾸면 어긋난다. 여기서는 **아무 치수도 안 벤다**: 그리드가 제 폭과 첫 카드의 폭을
8
+ * 재서 센다(`TrainingBlockGrid`). 순수 셈만 여기 둔다.
9
+ *
10
+ * 규칙(사용자: *"무조건이 아니라 개행될 때만"*): 한 줄에 다 들면 그대로, 안 들어 접히면 줄 수를
11
+ * 먼저 정하고(`ceil(n / 한줄)`) 고르게 나눈다 — 한 줄 넷이면 5 → 3+2 · 6 → 3+3 · 7 → 4+3.
12
+ */
13
+ /** 이 폭에 카드가 한 줄에 몇 장 드나(최소 1) */
14
+ export function columnsFitting(width, cardWidth, gap) {
15
+ if (!(width > 0) || !(cardWidth > 0))
16
+ return 1;
17
+ return Math.max(1, Math.floor((width + gap) / (cardWidth + gap)));
18
+ }
19
+ /** 장수와 한 줄 최대로 칸 수 — 한 줄에 다 들면 그대로, 접히면 고르게 */
20
+ export function balancedColumns(count, perRow) {
21
+ if (!(count > 0))
22
+ return 1;
23
+ const 한줄 = Math.max(1, Math.floor(perRow));
24
+ if (count <= 한줄)
25
+ return count;
26
+ const 줄수 = Math.ceil(count / 한줄);
27
+ return Math.max(1, Math.ceil(count / 줄수));
28
+ }
@@ -13,6 +13,7 @@ export type { ContrastFilterOpts } from './utils/wordContrast';
13
13
  export type { CompositionFilter } from './utils/wordTrainingRuntimeTypes';
14
14
  export { parseSyllable, composeSyllable, createDecompCache, CHOSEONG, JUNGSEONG, JONGSEONG } from './utils/koreanSyllable';
15
15
  export type { Syllable } from './utils/koreanSyllable';
16
+ export { balancedColumns, columnsFitting } from './utils/blockGrid';
16
17
  export { toStandardPronunciation } from './utils/koreanPronunciation';
17
18
  export { finalToPhoneme } from './utils/phonemeConvert';
18
19
  export { withIga, withEulReul, withEunNeun, withWaGwa, withEuRo, pickIga, pickEulReul, pickEunNeun, pickWaGwa, pickEuRo } from './utils/josa';
@@ -8,6 +8,7 @@ export { configureImageResolver, getImageResolver, checkImageExists, getImageExi
8
8
  export { buildPairIndex, getPairIndex, clearPairIndexCache } from './utils/wordPairIndex';
9
9
  export { minimalPairDistractors, filterContrastCandidates, hasStructureContrast, buildMultiPartPosContrast, hasSel } from './utils/wordContrast';
10
10
  export { parseSyllable, composeSyllable, createDecompCache, CHOSEONG, JUNGSEONG, JONGSEONG } from './utils/koreanSyllable';
11
+ export { balancedColumns, columnsFitting } from './utils/blockGrid';
11
12
  export { toStandardPronunciation } from './utils/koreanPronunciation';
12
13
  export { finalToPhoneme } from './utils/phonemeConvert';
13
14
  export { withIga, withEulReul, withEunNeun, withWaGwa, withEuRo, pickIga, pickEulReul, pickEunNeun, pickWaGwa, pickEuRo } from './utils/josa';
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.309.0";
49
+ export declare const CORE_VERSION = "0.311.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.309.0';
49
+ export const CORE_VERSION = '0.311.0';