@ihabdevteam/core 0.307.0 → 0.309.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.
@@ -1651,7 +1651,7 @@ export declare function copyText(text: string): Promise<boolean>;
1651
1651
  *
1652
1652
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
1653
1653
  */
1654
- export declare const CORE_VERSION = "0.307.0";
1654
+ export declare const CORE_VERSION = "0.309.0";
1655
1655
 
1656
1656
  /** @public */
1657
1657
  declare const coreDefault: Record<string, unknown>;
@@ -1915,13 +1915,17 @@ export declare function dateToDb(app: string | null | undefined): string | undef
1915
1915
  export declare function DebugResultLauncher({ enabled, renderResultPopup }?: {
1916
1916
  /** 켜야 단추가 보인다. 기본은 꺼짐 — 배포 화면에 실수로 남지 않게. */
1917
1917
  enabled?: boolean;
1918
- renderResultPopup?: (props: {
1919
- score?: number | null;
1920
- contextsLength?: number | null;
1921
- results?: unknown[];
1922
- onReplay?: () => void;
1923
- onClose?: () => void;
1924
- }) => ReactNode;
1918
+ /**
1919
+ * 결과 창을 그리는 함수 — **훈련 창이 쓰는 것과 같은 모양**을 가리킨다.
1920
+ *
1921
+ * 전에는 여기에 모양을 **손수 베껴** 두었고 `pending`(결과를 아직 세는 중) 한 칸이
1922
+ * 빠져 있었다. 그 칸이 없어도 아무것도 안 깨진다 — 있어도 되는 칸이라 **조용하다.**
1923
+ * 다만 창에 칸이 늘 때 이 자리만 옛 모양으로 남는다. (104) 를 세며 찾은 **셋째 자리**다.
1924
+ *
1925
+ * 이 단추는 다 만들어진 자료를 바로 띄우니 `pending` 을 안 넘긴다 — 없어도 되는 칸이라
1926
+ * 넘기지 않는 것이 맞다.
1927
+ */
1928
+ renderResultPopup?: TrainingPagePopupProps['renderResultPopup'];
1925
1929
  }): JSX.Element | null;
1926
1930
 
1927
1931
  declare type Decomp = (ReturnType<typeof parseSyllable>)[];
@@ -8281,6 +8285,18 @@ export declare interface TrainingBlockButtonPopupProps {
8281
8285
  * 못은 `test/roundFinishedResultShape.test-d.ts` 가 **두 쪽을 맞대어** 센다.
8282
8286
  */
8283
8287
  onRoundFinished?: (result: TrainingCompleteResult) => void;
8288
+ /**
8289
+ * 결과창을 손수 그린다 — **없으면 기본 결과 창**(`TrainingResultPopup`). `TrainingPagePopup`
8290
+ * 으로 그대로 전달된다(말귀 요청 (104), 0.308.0).
8291
+ *
8292
+ * **값은 `...rest` 로 흘러 이미 가고 있었고 타입에만 없었다** — `onRoundFinished` 가
8293
+ * 0.300.0 에 겪은 것과 **같은 꼴, 다른 칸**이다(그쪽은 *돌려주는* 덩이, 이쪽은 *넘겨주는*
8294
+ * 꾸러미). 쓰는 쪽 셋이 전부 같은 `as Parameters<typeof defaultPopupRenderer>[0]` 로
8295
+ * 눌러 왔다(말귀 실측: 셋을 걷으니 셋이 함께 `TS2353`).
8296
+ *
8297
+ * 모양을 **베끼지 않고** 창의 것을 그대로 가리킨다 — 창에 칸이 늘면 여기도 같이 는다.
8298
+ */
8299
+ renderResultPopup?: TrainingPagePopupProps['renderResultPopup'];
8284
8300
  popupTitle: string;
8285
8301
  popupSubTitle?: string;
8286
8302
  depth?: string;
@@ -27,11 +27,14 @@
27
27
  color: var(--color-darkGray);
28
28
  }
29
29
 
30
- /* 클릭 가능한 경우 호버 강조 */
31
- .count-tile--clickable { cursor: pointer; }
32
- .count-tile--clickable:hover {
30
+ /* 반응하는 타일(누르기 · 담긴 훈련 미리보기 · 부르는 쪽 툴팁)의 호버 — **호버 모양은 여기 한 곳**.
31
+ `--clickable` 은 커서만 지킨다. 전에는 `--clickable:hover` 에만 있어 얹기만 하는 타일이
32
+ 반응하는 자리인데 반응하는 것처럼 안 보였다(말귀 (105), 2026-10-10). */
33
+ .count-tile--interactive:hover {
33
34
  background: var(--color-white);
35
+ border-color: var(--color-lightGray);
34
36
  }
37
+ .count-tile--clickable { cursor: pointer; }
35
38
 
36
39
  /* 모바일 — 기본 60×60. 72px일 땐 카드 안에 4개가 못 들어가 3+1로 어정쩡하게 갈라졌다.
37
40
  60px이면 4개(60×4 + 6×3 = 258)가 카드 안에 한 줄로 들어간다. */
@@ -16,7 +16,17 @@ const 건너가는틈_MS = 200;
16
16
  * @public
17
17
  */
18
18
  export default function CountTile({ icon, count, title, plan, tooltip = true, className = '', onClick, onMouseEnter, onMouseLeave }) {
19
- const cls = cx('count-tile', onClick && 'count-tile--clickable', className);
19
+ /**
20
+ * **반응하는 타일이면 반응하는 것처럼 보인다**(말귀 요청 (105), 사용자 보고 2026-10-10).
21
+ *
22
+ * 전에는 `onClick` 이 있을 때만 호버 모양을 줬다. 그런데 이 타일이 반응하는 길은 셋이다 —
23
+ * 누르기 · 얹으면 담긴 훈련이 뜸(`tooltip && plan`) · 부르는 쪽이 제 툴팁을 띄움(`onMouseEnter`).
24
+ * 회원 숙제 카드는 셋째 길만 쓰고 매니저 숙제 카드는 둘째 길만 써서, **반응하는 자리인데
25
+ * 반응하는 것처럼 안 보였다.** 호버 모양은 `--interactive` 가 맡고 `--clickable` 은 커서만
26
+ * 지킨다 — 누르는 타일과 얹는 타일이 **같은 호버**를 쓰고 커서만 갈린다.
27
+ */
28
+ const 반응하나 = Boolean(onClick) || Boolean(onMouseEnter) || (tooltip && Boolean(plan));
29
+ const cls = cx('count-tile', 반응하나 && 'count-tile--interactive', onClick && 'count-tile--clickable', className);
20
30
  const ref = useRef(null);
21
31
  const [anchorRect, setAnchorRect] = useState(null);
22
32
  const 늦춤 = useRef(null);
@@ -1,5 +1,5 @@
1
- import { type ReactNode } from 'react';
2
1
  import './DebugTooltip.css';
2
+ import type { TrainingPagePopupProps } from '../Training/TrainingPagePopup';
3
3
  /**
4
4
  * 결과 팝업을 실제 훈련 없이 열어보는 개발용 단추 넷.
5
5
  *
@@ -13,11 +13,15 @@ import './DebugTooltip.css';
13
13
  export default function DebugResultLauncher({ enabled, renderResultPopup }?: {
14
14
  /** 켜야 단추가 보인다. 기본은 꺼짐 — 배포 화면에 실수로 남지 않게. */
15
15
  enabled?: boolean;
16
- renderResultPopup?: (props: {
17
- score?: number | null;
18
- contextsLength?: number | null;
19
- results?: unknown[];
20
- onReplay?: () => void;
21
- onClose?: () => void;
22
- }) => ReactNode;
16
+ /**
17
+ * 결과 창을 그리는 함수 — **훈련 창이 쓰는 것과 같은 모양**을 가리킨다.
18
+ *
19
+ * 전에는 여기에 모양을 **손수 베껴** 두었고 `pending`(결과를 아직 세는 중) 한 칸이
20
+ * 빠져 있었다. 그 칸이 없어도 아무것도 안 깨진다 — 있어도 되는 칸이라 **조용하다.**
21
+ * 다만 창에 칸이 늘 때 이 자리만 옛 모양으로 남는다. (104) 를 세며 찾은 **셋째 자리**다.
22
+ *
23
+ * 이 단추는 다 만들어진 자료를 바로 띄우니 `pending` 을 안 넘긴다 — 없어도 되는 칸이라
24
+ * 넘기지 않는 것이 맞다.
25
+ */
26
+ renderResultPopup?: TrainingPagePopupProps['renderResultPopup'];
23
27
  }): import("react").JSX.Element | null;
@@ -55,6 +55,14 @@
55
55
  cursor: default;
56
56
  }
57
57
 
58
+ /* 올리면 설명 말풍선이 뜨는 자리의 호버 — **아이콘 색으로 1px 테두리**(말귀 (106), 사용자 지시
59
+ 2026-10-10). 안쪽 그림자라 40px 이 안 흔들린다. 표는 `--interactive` 에만 산다 —
60
+ `useHover={false}` 거나 보여 줄 글이 없으면 말풍선이 안 서니 테두리도 안 선다. 맨
61
+ `.category-icon__wrap:hover` 로 적지 말 것(그러면 반응 안 하는 자리가 반응하는 척한다). */
62
+ .category-icon__wrap--interactive:hover .category-icon {
63
+ box-shadow: inset 0 0 0 1px var(--t-item-fg, var(--color-lightGray));
64
+ }
65
+
58
66
  /* ── Hover tooltip ────────────────────────────────────── */
59
67
  .category-icon__tooltip {
60
68
  white-space: nowrap;
@@ -25,6 +25,14 @@ export default function CategoryIcon({ icon, label, tooltip, wrapperStyle, color
25
25
  };
26
26
  }, [wrapperStyle, color, bgColor]);
27
27
  const tipText = tooltip || label;
28
+ /**
29
+ * **반응하는 자리면 반응하는 것처럼 보인다**(말귀 요청 (106), 사용자 지시 2026-10-10 —
30
+ * *"category icon 에 color 로 border 1 표현"*). 올리면 설명 말풍선이 뜨는데 상자는 그대로였다.
31
+ * 표는 CSS 의 맨 `:hover` 로 못 준다 — `useHover={false}` 면 말풍선이 안 서는데 테두리만
32
+ * 서면 (105) 의 반대 거짓이다. 그래서 **말풍선이 설 때만** 이 표를 붙이고, 호버 테두리는
33
+ * `--interactive` 에만 산다. 보여 줄 글이 없으면(`tipText` 없음) 올려도 아무 일이 없으니 표도 없다.
34
+ */
35
+ const 반응하나 = useHover && Boolean(tipText);
28
36
  const [anchorRect, setAnchorRect] = useState(null);
29
37
  useLayoutEffect(() => {
30
38
  if (hover)
@@ -77,5 +85,5 @@ export default function CategoryIcon({ icon, label, tooltip, wrapperStyle, color
77
85
  setHover(v => !v);
78
86
  }
79
87
  }, []);
80
- return (_jsxs("span", { ref: ref, className: "category-icon__wrap", onPointerEnter: handleHoverOn, onPointerLeave: handleHoverOff, onFocus: handleHoverOn, onBlur: handleHoverOff, onKeyDown: handleKeyDown, role: "button", tabIndex: 0, "aria-label": typeof tipText === 'string' ? t('common.descriptionFor', { name: tipText, defaultValue: `${tipText} 설명` }) : undefined, "aria-haspopup": !!tipText, "aria-expanded": hover, "data-tooltip-ignore": true, children: [_jsx(Icon, { icon: icon, label: label, className: cls, imgClassName: "category-icon__image", maskClassName: "category-icon__mask", svgClassName: "category-icon__svg", wrapperStyle: resolvedWrapperStyle, size: svgSize }), hover && tipText && (_jsx(InfoTooltip, { anchorRect: anchorRect, text: typeof tipText === 'string' ? tipText : undefined, offset: 8, onClose: handleTooltipClose, className: "category-icon__tooltip", ariaLabel: typeof tipText === 'string' ? tipText : undefined }))] }));
88
+ return (_jsxs("span", { ref: ref, className: cx('category-icon__wrap', 반응하나 && 'category-icon__wrap--interactive'), onPointerEnter: handleHoverOn, onPointerLeave: handleHoverOff, onFocus: handleHoverOn, onBlur: handleHoverOff, onKeyDown: handleKeyDown, role: "button", tabIndex: 0, "aria-label": typeof tipText === 'string' ? t('common.descriptionFor', { name: tipText, defaultValue: `${tipText} 설명` }) : undefined, "aria-haspopup": !!tipText, "aria-expanded": hover, "data-tooltip-ignore": true, children: [_jsx(Icon, { icon: icon, label: label, className: cls, imgClassName: "category-icon__image", maskClassName: "category-icon__mask", svgClassName: "category-icon__svg", wrapperStyle: resolvedWrapperStyle, size: svgSize }), hover && tipText && (_jsx(InfoTooltip, { anchorRect: anchorRect, text: typeof tipText === 'string' ? tipText : undefined, offset: 8, onClose: handleTooltipClose, className: "category-icon__tooltip", ariaLabel: typeof tipText === 'string' ? tipText : undefined }))] }));
81
89
  }
@@ -1,6 +1,6 @@
1
1
  import type { CSSProperties, ReactNode } from 'react';
2
2
  import type { TrainingItem } from '../../types/training';
3
- import type { TrainingCompleteResult } from './TrainingPagePopup';
3
+ import type { TrainingCompleteResult, TrainingPagePopupProps } from './TrainingPagePopup';
4
4
  /** @public */
5
5
  export interface TrainingBlockButtonPopupProps {
6
6
  trainingData: TrainingItem | null;
@@ -28,6 +28,18 @@ export interface TrainingBlockButtonPopupProps {
28
28
  * 못은 `test/roundFinishedResultShape.test-d.ts` 가 **두 쪽을 맞대어** 센다.
29
29
  */
30
30
  onRoundFinished?: (result: TrainingCompleteResult) => void;
31
+ /**
32
+ * 결과창을 손수 그린다 — **없으면 기본 결과 창**(`TrainingResultPopup`). `TrainingPagePopup`
33
+ * 으로 그대로 전달된다(말귀 요청 (104), 0.308.0).
34
+ *
35
+ * **값은 `...rest` 로 흘러 이미 가고 있었고 타입에만 없었다** — `onRoundFinished` 가
36
+ * 0.300.0 에 겪은 것과 **같은 꼴, 다른 칸**이다(그쪽은 *돌려주는* 덩이, 이쪽은 *넘겨주는*
37
+ * 꾸러미). 쓰는 쪽 셋이 전부 같은 `as Parameters<typeof defaultPopupRenderer>[0]` 로
38
+ * 눌러 왔다(말귀 실측: 셋을 걷으니 셋이 함께 `TS2353`).
39
+ *
40
+ * 모양을 **베끼지 않고** 창의 것을 그대로 가리킨다 — 창에 칸이 늘면 여기도 같이 는다.
41
+ */
42
+ renderResultPopup?: TrainingPagePopupProps['renderResultPopup'];
31
43
  popupTitle: string;
32
44
  popupSubTitle?: string;
33
45
  depth?: string;
@@ -36,7 +36,16 @@ load) {
36
36
  return mod;
37
37
  }
38
38
  /* 여기 오는 길은 사실상 하나뿐이다 — 청크를 못 받아 복구가 도는 중. 새 판을
39
- 기다린다. 영영 resolve 하지 않는 약속을 돌려 React 가 이 경계를 건드리지 않게 한다. */
39
+ 기다린다. 영영 resolve 하지 않는 약속을 돌려 React 가 이 경계를 건드리지 않게 한다.
40
+
41
+ **`load()` 가 거절되는 길은 여기 안 온다 — 일부러다.** 위 `await` 가 그대로 던진다.
42
+ 막이(10초) 안의 두 번째 실패가 그 길이다(새 index 의 조각도 아직 CDN 에 안 퍼진 때 —
43
+ 말귀 prod 2026-10-02, 8초 안에 두 번). 그때 preventDefault 가 안 되어 import 가 거절되는데,
44
+ 여기서 `skewReloadPending()` 을 보고 영원한 약속을 돌려주면 **막이가 풀려도 영영 빈 화면**
45
+ 이다 — 새로고침은 이미 다 썼고 다시 올 것이 없다. 던져서 경계까지 가야 한다.
46
+ 그리고 React `lazy` 는 한 번 거절되면 **굳는다**(`_status = 2`) — 경계의 "다시 시도" 가
47
+ 같은 거절을 다시 받는다. 그 오류를 받은 경계는 **새로고침을 쥐어야** 한다(CONSUMING.md
48
+ §2-0-6). 둘 다 `test/chunkSkewLazyPage0917.test.tsx` 의 못이다. */
40
49
  if (skewReloadPending())
41
50
  return new Promise(() => { });
42
51
  throw new Error(`[lazyPage] ${name}: 화면 모듈을 받지 못했습니다(default 내보내기 없음). ` +
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.307.0";
49
+ export declare const CORE_VERSION = "0.309.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.307.0';
49
+ export const CORE_VERSION = '0.309.0';