@ihabdevteam/core 0.187.0 → 0.188.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.
@@ -1606,7 +1606,7 @@ export declare function copyText(text: string): Promise<boolean>;
1606
1606
  *
1607
1607
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
1608
1608
  */
1609
- export declare const CORE_VERSION = "0.187.0";
1609
+ export declare const CORE_VERSION = "0.188.0";
1610
1610
 
1611
1611
  /** @public */
1612
1612
  declare const coreDefault: Record<string, unknown>;
@@ -1690,11 +1690,11 @@ export declare type CoreScopeStorageConfig = {
1690
1690
  * 72×72 정사각 타일. 좌상단에 아이콘, 우하단에 숫자를 배치한다.
1691
1691
  * 훈련 카드의 타입별 문항 수 표시 등에 사용.
1692
1692
  *
1693
- * 마우스를 얹으면 `title` 이 툴팁으로 뜬다 — 늦춤은 다른 자리와 같은 상수를 쓴다
1694
- * (지나가다 스치는 것에 번쩍이지 않게). 키보드로 옮겨 와도(누를 수 있는 타일) 뜬다.
1693
+ * `plan` 을 주면 마우스를 얹었을 때 **담긴 훈련**(`TrainingTooltip`)이 뜬다 — 늦춤은 다른
1694
+ * 자리와 같은 상수를 쓴다(지나가다 스치는 것에 번쩍이지 않게).
1695
1695
  * @public
1696
1696
  */
1697
- export declare function CountTile({ icon, count, title, tooltip, className, onClick, onMouseEnter, onMouseLeave }: CountTileProps): JSX.Element;
1697
+ export declare function CountTile({ icon, count, title, plan, tooltip, className, onClick, onMouseEnter, onMouseLeave }: CountTileProps): JSX.Element;
1698
1698
 
1699
1699
  /** @public */
1700
1700
  export declare interface CountTileProps {
@@ -1703,21 +1703,25 @@ export declare interface CountTileProps {
1703
1703
  /** 우측 하단 숫자 */
1704
1704
  count: number | string;
1705
1705
  /**
1706
- * 갈래 이름 — 낭독기가 읽는 이름이자 **눈으로 보는 사람에게 뜨는 툴팁의 글**이다.
1706
+ * 갈래 이름 — 낭독기가 읽는 이름이다(`"이름 수"` 로 읽힌다).
1707
1707
  *
1708
1708
  * **필수다.** 72×72 안에는 아이콘과 수뿐이라, 이 값이 없으면 그 아이콘이 무슨 갈래인지
1709
- * 알 길이 화면에도 낭독기에도 없다(말귀 '나의 단어훈련' 카드에 타일이 열 개 선다).
1710
- * 예전에는 선택값이었고 `aria-label` 로만 나가, 마우스 쓰는 사람은 **아무것도 못 봤다**
1711
- * (사용자 지시 2026-09-23: "호버시 tooltip 은 무조건 나와야 해").
1709
+ * 낭독기로는 알 길이 없다(말귀 '나의 단어훈련' 카드에 타일이 열 개 선다).
1712
1710
  */
1713
1711
  title: string;
1714
1712
  /**
1715
- * 호버 툴팁을 끈다(기본 켬).
1713
+ * 호버하면 보여 줄 **담긴 훈련**.
1716
1714
  *
1717
- * 부르는 쪽이 **제 툴팁**을 띄우는 자리에서만 쓴다 — 말귀의 숙제 확인창 미리보기는
1718
- * `onMouseEnter` 로 무엇이 담겼는지 보여 주는 큰 툴팁(`TrainingTooltip`)을 띄우는데,
1719
- * 여기 기본 툴팁까지 뜨면 둘이 겹친다. 그 자리만 끈다.
1715
+ * 주면 마우스를 얹었을 때 `TrainingTooltip` 이 뜬다 — 그 안에 훈련 블록이 서고, 블록의
1716
+ * 갈래 아이콘(`CategoryIcon`)에 다시 마우스를 얹으면 제 이름이 작은 툴팁으로 뜬다
1717
+ * (사용자 지시 2026-09-23). 안 주면 아무것도 안 뜬다 — 타일 이름만 띄우는 글자 툴팁은
1718
+ * **두지 않는다**(0.187.0 에 잠깐 있었고, 그건 요청을 잘못 읽은 것이었다).
1719
+ *
1720
+ * 이 타일이 세는 그 갈래의 항목만 담은 `plan` 을 준다 — `TrainingTooltip` 은 받은 `plan.items`
1721
+ * 를 갈래별로 묶어 그리므로, 카드 전체의 `plan` 을 그대로 주면 타일마다 같은 것이 뜬다.
1720
1722
  */
1723
+ plan?: TrainingPlan;
1724
+ /** 호버 툴팁을 끈다(기본 켬) — 부르는 쪽이 제 툴팁을 띄우는 자리용 */
1721
1725
  tooltip?: boolean;
1722
1726
  className?: string;
1723
1727
  /** 클릭 핸들러 — 설정 시 버튼처럼 동작 */
@@ -1,4 +1,5 @@
1
1
  import './CountTile.css';
2
+ import type { TrainingPlan } from '../types/training';
2
3
  import type { ReactNode, MouseEvent } from 'react';
3
4
  /** @public */
4
5
  export interface CountTileProps {
@@ -7,21 +8,25 @@ export interface CountTileProps {
7
8
  /** 우측 하단 숫자 */
8
9
  count: number | string;
9
10
  /**
10
- * 갈래 이름 — 낭독기가 읽는 이름이자 **눈으로 보는 사람에게 뜨는 툴팁의 글**이다.
11
+ * 갈래 이름 — 낭독기가 읽는 이름이다(`"이름 수"` 로 읽힌다).
11
12
  *
12
13
  * **필수다.** 72×72 안에는 아이콘과 수뿐이라, 이 값이 없으면 그 아이콘이 무슨 갈래인지
13
- * 알 길이 화면에도 낭독기에도 없다(말귀 '나의 단어훈련' 카드에 타일이 열 개 선다).
14
- * 예전에는 선택값이었고 `aria-label` 로만 나가, 마우스 쓰는 사람은 **아무것도 못 봤다**
15
- * (사용자 지시 2026-09-23: "호버시 tooltip 은 무조건 나와야 해").
14
+ * 낭독기로는 알 길이 없다(말귀 '나의 단어훈련' 카드에 타일이 열 개 선다).
16
15
  */
17
16
  title: string;
18
17
  /**
19
- * 호버 툴팁을 끈다(기본 켬).
18
+ * 호버하면 보여 줄 **담긴 훈련**.
20
19
  *
21
- * 부르는 쪽이 **제 툴팁**을 띄우는 자리에서만 쓴다 — 말귀의 숙제 확인창 미리보기는
22
- * `onMouseEnter` 로 무엇이 담겼는지 보여 주는 큰 툴팁(`TrainingTooltip`)을 띄우는데,
23
- * 여기 기본 툴팁까지 뜨면 둘이 겹친다. 그 자리만 끈다.
20
+ * 주면 마우스를 얹었을 때 `TrainingTooltip` 이 뜬다 — 그 안에 훈련 블록이 서고, 블록의
21
+ * 갈래 아이콘(`CategoryIcon`)에 다시 마우스를 얹으면 제 이름이 작은 툴팁으로 뜬다
22
+ * (사용자 지시 2026-09-23). 안 주면 아무것도 안 뜬다 — 타일 이름만 띄우는 글자 툴팁은
23
+ * **두지 않는다**(0.187.0 에 잠깐 있었고, 그건 요청을 잘못 읽은 것이었다).
24
+ *
25
+ * 이 타일이 세는 그 갈래의 항목만 담은 `plan` 을 준다 — `TrainingTooltip` 은 받은 `plan.items`
26
+ * 를 갈래별로 묶어 그리므로, 카드 전체의 `plan` 을 그대로 주면 타일마다 같은 것이 뜬다.
24
27
  */
28
+ plan?: TrainingPlan;
29
+ /** 호버 툴팁을 끈다(기본 켬) — 부르는 쪽이 제 툴팁을 띄우는 자리용 */
25
30
  tooltip?: boolean;
26
31
  className?: string;
27
32
  /** 클릭 핸들러 — 설정 시 버튼처럼 동작 */
@@ -33,8 +38,8 @@ export interface CountTileProps {
33
38
  * 72×72 정사각 타일. 좌상단에 아이콘, 우하단에 숫자를 배치한다.
34
39
  * 훈련 카드의 타입별 문항 수 표시 등에 사용.
35
40
  *
36
- * 마우스를 얹으면 `title` 이 툴팁으로 뜬다 — 늦춤은 다른 자리와 같은 상수를 쓴다
37
- * (지나가다 스치는 것에 번쩍이지 않게). 키보드로 옮겨 와도(누를 수 있는 타일) 뜬다.
41
+ * `plan` 을 주면 마우스를 얹었을 때 **담긴 훈련**(`TrainingTooltip`)이 뜬다 — 늦춤은 다른
42
+ * 자리와 같은 상수를 쓴다(지나가다 스치는 것에 번쩍이지 않게).
38
43
  * @public
39
44
  */
40
- export default function CountTile({ icon, count, title, tooltip, className, onClick, onMouseEnter, onMouseLeave }: CountTileProps): import("react").JSX.Element;
45
+ export default function CountTile({ icon, count, title, plan, tooltip, className, onClick, onMouseEnter, onMouseLeave }: CountTileProps): import("react").JSX.Element;
@@ -4,47 +4,79 @@ import { useCallback, useEffect, useRef, useState } from 'react';
4
4
  import { cx } from '../utils/cx';
5
5
  import './CountTile.css';
6
6
  import Icon from './Icon';
7
- import InfoTooltip from './Tooltip/InfoTooltip';
8
- import { TRAINING_TOOLTIP_HOVER_DELAY_MS } from './Tooltip/TrainingTooltip';
7
+ import TrainingTooltip, { TRAINING_TOOLTIP_HOVER_DELAY_MS } from './Tooltip/TrainingTooltip';
8
+ /** 타일과 툴팁 사이 틈(12px)을 건너는 동안 툴팁을 살려 두는 시간 */
9
+ const 건너가는틈_MS = 200;
9
10
  /**
10
11
  * 72×72 정사각 타일. 좌상단에 아이콘, 우하단에 숫자를 배치한다.
11
12
  * 훈련 카드의 타입별 문항 수 표시 등에 사용.
12
13
  *
13
- * 마우스를 얹으면 `title` 이 툴팁으로 뜬다 — 늦춤은 다른 자리와 같은 상수를 쓴다
14
- * (지나가다 스치는 것에 번쩍이지 않게). 키보드로 옮겨 와도(누를 수 있는 타일) 뜬다.
14
+ * `plan` 을 주면 마우스를 얹었을 때 **담긴 훈련**(`TrainingTooltip`)이 뜬다 — 늦춤은 다른
15
+ * 자리와 같은 상수를 쓴다(지나가다 스치는 것에 번쩍이지 않게).
15
16
  * @public
16
17
  */
17
- export default function CountTile({ icon, count, title, tooltip = true, className = '', onClick, onMouseEnter, onMouseLeave }) {
18
+ export default function CountTile({ icon, count, title, plan, tooltip = true, className = '', onClick, onMouseEnter, onMouseLeave }) {
18
19
  const cls = cx('count-tile', onClick && 'count-tile--clickable', className);
19
20
  const ref = useRef(null);
20
21
  const [anchorRect, setAnchorRect] = useState(null);
21
22
  const 늦춤 = useRef(null);
22
- const 닫기 = useCallback(() => {
23
+ const 닫힘 = useRef(null);
24
+ const 켜짐 = Boolean(anchorRect && plan && tooltip);
25
+ const 시계끄기 = useCallback(() => {
23
26
  if (늦춤.current) {
24
27
  clearTimeout(늦춤.current);
25
28
  늦춤.current = null;
26
29
  }
27
- setAnchorRect(null);
30
+ if (닫힘.current) {
31
+ clearTimeout(닫힘.current);
32
+ 닫힘.current = null;
33
+ }
28
34
  }, []);
35
+ const 닫기 = useCallback(() => { 시계끄기(); setAnchorRect(null); }, [시계끄기]);
29
36
  /* 늦춰서 연다 — 타일이 열 개 붙어 서는 자리라, 지나가기만 해도 뜨면 줄줄이 번쩍인다. */
30
- const 열기 = useCallback((바로) => {
31
- if (!tooltip)
37
+ const 열기 = useCallback(() => {
38
+ if (!tooltip || !plan)
32
39
  return;
33
- if (늦춤.current)
40
+ 시계끄기();
41
+ 늦춤.current = setTimeout(() => {
42
+ 늦춤.current = null;
43
+ setAnchorRect(ref.current?.getBoundingClientRect() ?? null);
44
+ }, TRAINING_TOOLTIP_HOVER_DELAY_MS);
45
+ }, [tooltip, plan, 시계끄기]);
46
+ /**
47
+ * **바로 닫지 않는다.** 툴팁은 타일에서 12px 떨어져 뜨므로, 타일을 벗어나자마자 닫으면
48
+ * 그 틈을 건너는 동안 사라져 **툴팁 안의 갈래 아이콘에 마우스를 올릴 수가 없다** — 그
49
+ * 아이콘에 다시 뜨는 작은 이름표가 이 툴팁을 다는 까닭의 절반이다(사용자 지시 2026-09-23).
50
+ * 잠깐 두고, 그 사이 툴팁 안으로 들어오면 아래 지킴이가 예약을 무른다.
51
+ */
52
+ const 닫기예약 = useCallback(() => {
53
+ if (늦춤.current) {
34
54
  clearTimeout(늦춤.current);
35
- const 띄우기 = () => setAnchorRect(ref.current?.getBoundingClientRect() ?? null);
36
- if (바로) {
37
55
  늦춤.current = null;
38
- 띄우기();
39
- return;
40
56
  }
41
- 늦춤.current = setTimeout(() => { 늦춤.current = null; 띄우기(); }, TRAINING_TOOLTIP_HOVER_DELAY_MS);
42
- }, [tooltip]);
57
+ if (닫힘.current)
58
+ return;
59
+ 닫힘.current = setTimeout(() => { 닫힘.current = null; setAnchorRect(null); }, 건너가는틈_MS);
60
+ }, []);
43
61
  /* 떠난 뒤에 타이머가 터지면 아무도 안 보는 자리에 툴팁이 남는다 */
44
- useEffect(() => () => { if (늦춤.current)
45
- clearTimeout(늦춤.current); }, []);
46
- useEffect(() => { if (!tooltip)
47
- 닫기(); }, [tooltip, 닫기]);
62
+ useEffect(() => () => { 시계끄기(); }, [시계끄기]);
63
+ useEffect(() => { if (!tooltip || !plan)
64
+ 닫기(); }, [tooltip, plan, 닫기]);
65
+ /* 툴팁이 떠 있는 동안 **포인터가 어디에 있는지** 본다 — 타일이나 툴팁 안이면 살려 두고,
66
+ 둘 다 아니면 닫기를 예약한다. 툴팁은 포털로 body 끝에 나가 타일의 자식이 아니라서,
67
+ 타일에 건 mouseleave 만으로는 "툴팁으로 건너갔다" 를 알 수 없다. */
68
+ useEffect(() => {
69
+ if (!켜짐)
70
+ return;
71
+ const 살릴자리 = (t) => t instanceof Element && (ref.current?.contains(t) || Boolean(t.closest('.tooltip-training')));
72
+ const 보기 = (e) => { if (살릴자리(e.target)) {
73
+ 시계끄기();
74
+ }
75
+ else
76
+ 닫기예약(); };
77
+ document.addEventListener('pointerover', 보기, true);
78
+ return () => document.removeEventListener('pointerover', 보기, true);
79
+ }, [켜짐, 시계끄기, 닫기예약]);
48
80
  return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: ref, className: cls, onClick: onClick ? (e) => { 닫기(); onClick(e); } : undefined,
49
81
  /* role="button" + tabIndex 만 있고 키 처리가 없으면, 키보드 사용자는 여기 멈추고도
50
82
  누를 수가 없다 — `<div>` 는 브라우저가 Enter·Space 를 대신 처리해 주지 않는다.
@@ -67,7 +99,5 @@ export default function CountTile({ icon, count, title, tooltip = true, classNam
67
99
  onPointerEnter: (e) => { if (e.pointerType === 'touch' || e.pointerType === 'pen')
68
100
  닫기();
69
101
  else
70
- 열기(false); }, onMouseEnter: onMouseEnter, onMouseLeave: (e) => { 닫기(); onMouseLeave?.(e); },
71
- /* 키보드로 옮겨 온 사람에게도 보여야 한다 — 멈춘 자리라 늦출 까닭이 없어 바로 연다. */
72
- onFocus: onClick ? () => 열기(true) : undefined, onBlur: onClick ? 닫기 : undefined, role: onClick ? 'button' : 'img', tabIndex: onClick ? 0 : undefined, "aria-label": `${title} ${count}`, children: [_jsx(Icon, { icon: icon, size: 20, className: "count-tile__icon" }), _jsx("span", { className: "count-tile__count h5", "aria-hidden": "true", children: count })] }), anchorRect && _jsx(InfoTooltip, { anchorRect: anchorRect, text: title })] }));
102
+ 열기(); }, onMouseEnter: onMouseEnter, onMouseLeave: (e) => { 닫기예약(); onMouseLeave?.(e); }, role: onClick ? 'button' : 'img', tabIndex: onClick ? 0 : undefined, "aria-label": `${title} ${count}`, children: [_jsx(Icon, { icon: icon, size: 20, className: "count-tile__icon" }), _jsx("span", { className: "count-tile__count h5", "aria-hidden": "true", children: count })] }), anchorRect && plan && _jsx(TrainingTooltip, { plan: plan, anchorRect: anchorRect, onClose: 닫기 })] }));
73
103
  }
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.187.0";
49
+ export declare const CORE_VERSION = "0.188.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.187.0';
49
+ export const CORE_VERSION = '0.188.0';