@ihabdevteam/core 0.224.0 → 0.226.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.
@@ -1626,7 +1626,7 @@ export declare function copyText(text: string): Promise<boolean>;
1626
1626
  *
1627
1627
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
1628
1628
  */
1629
- export declare const CORE_VERSION = "0.224.0";
1629
+ export declare const CORE_VERSION = "0.226.0";
1630
1630
 
1631
1631
  /** @public */
1632
1632
  declare const coreDefault: Record<string, unknown>;
@@ -55,10 +55,18 @@
55
55
  .date-picker--open .date-picker__icon { color: var(--color-main); }
56
56
 
57
57
  /* Dropdown */
58
+ /* **자리는 인라인 스타일이 쥔다 — 여기서 `top`·`left`·`position` 을 적지 않는다.**
59
+ 한때 `position: absolute; top: calc(100% + 6px); left: 0` 이 여기 있었다. 판이
60
+ 칸 바로 아래 붙어 있던 시절의 값인데, 그 뒤 판은 `document.body` 로 포탈되어
61
+ `position: fixed` 로 자리를 받게 됐다. 그런데 이 `top` 만 남았다.
62
+
63
+ 그래서 **위로 뒤집기가 그려지지 않았다.** 인라인이 `position`·`left`·`bottom` 은
64
+ 덮지만 `top` 은 지우지 않는다 — 높이가 `auto` 인 요소에 `top` 과 `bottom` 이 둘 다
65
+ 있으면 **`top` 이 자리를 정하고 높이는 그 둘 사이로 눌린다.** `fixed` 아래의
66
+ `calc(100% + 6px)` 은 창 높이의 100% 라, 창이 1278 이면 `top` 이 1284 — 화면 바로
67
+ 아래다. 판은 거기 서고 높이는 음수로 눌려 26px 이 됐다(말귀 실측 2026-10-01).
68
+ 셈은 내내 옳았고 그려지지 않았을 뿐이다. */
58
69
  .date-picker__dropdown {
59
- position: absolute;
60
- top: calc(100% + 6px);
61
- left: 0;
62
70
  z-index: 200;
63
71
  background: var(--color-white);
64
72
  border: 1px solid var(--color-whiteLine);
@@ -65,10 +65,21 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
65
65
  const vw = window.innerWidth;
66
66
  /* 판의 실제 크기 — 열린 뒤에 재므로 있다. 없으면(첫 계산 전) 어림값 */
67
67
  const 판 = typeof document !== 'undefined' ? document.getElementById(panelId) : null;
68
- const dropdownH = 판?.offsetHeight || 300;
68
+ /* **우리가 얹은 자리에 흔들리지 않는 높이를 쓴다.**
69
+ `offsetHeight` 만 보면 우리가 방금 얹은 `top`/`bottom` 이 상자를 눌렀을 때 그
70
+ 눌린 값을 되읽는다 — 그 값으로 다시 자리를 잡고, 자리가 다시 높이를 바꾸어
71
+ 두 꼴을 오간다(실측: 306 ↔ 26). `scrollHeight` 는 눌려도 **안엣것의 높이**를
72
+ 그대로 내놓으므로 둘 중 큰 쪽을 쓴다. 판이 아직 없으면 어림값. */
73
+ const dropdownH = Math.max(판?.offsetHeight || 0, 판?.scrollHeight || 0) || 300;
69
74
  const 판폭 = Math.max(rect.width, 판?.offsetWidth || 0, 260);
70
75
  const spaceBelow = vh - rect.bottom;
71
- const above = spaceBelow < dropdownH && rect.top > dropdownH;
76
+ const spaceAbove = rect.top;
77
+ /* **아래가 모자라면 넓은 쪽으로 편다**(말귀 요청 ㊵, 2026-10-01).
78
+ 한때는 `rect.top > dropdownH`, 곧 **위가 통째로 남을 때만** 뒤집었다. 그래서 위도
79
+ 아래도 다 모자라면 늘 아래로 폈고 — 칸이 창 바닥 줄에 있으면 거의 늘 그 꼴이다 —
80
+ 아래로 삐져나갔다. 모자라는 것이 정해졌다면 **덜 모자라는 쪽**이 낫다.
81
+ `6` 과 `여백` 은 아래 자리 잡기와 같은 값이다. */
82
+ const above = spaceBelow < dropdownH && spaceAbove > spaceBelow;
72
83
  /* **창 안으로 조인다** — 스크롤을 따라 다시 잴 때 트리거가 화면 밖으로 나가면 달력이 창 위·아래 밖까지 따라가
73
84
  고를 수 없었다. 판이 창보다 크면 위 여백에 붙인다. */
74
85
  const 여백 = 8;
@@ -78,9 +89,14 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
78
89
  left: 조이기(rect.left, vw - 판폭 - 여백),
79
90
  width: rect.width,
80
91
  minWidth: 260,
92
+ /* **둘 다 적는다 — 쓰는 쪽만 적으면 안 된다.**
93
+ 안 쓰는 쪽을 비워 두면 스타일시트에 남은 `top`(또는 `bottom`)이 그대로 살아
94
+ 자리를 가로챈다. 높이가 `auto` 인 요소에 `top` 과 `bottom` 이 둘 다 있으면
95
+ `top` 이 이기고 높이는 그 둘 사이로 눌린다 — 판이 26px 으로 쪼그라들어
96
+ 화면 밖에 섰다(말귀 실측 2026-10-01). `auto` 로 **지운다.** */
81
97
  ...(above
82
- ? { bottom: 조이기(vh - rect.top + 6, vh - dropdownH - 여백) }
83
- : { top: 조이기(rect.bottom + 6, vh - dropdownH - 여백) }),
98
+ ? { top: 'auto', bottom: 조이기(vh - rect.top + 6, vh - dropdownH - 여백) }
99
+ : { bottom: 'auto', top: 조이기(rect.bottom + 6, vh - dropdownH - 여백) }),
84
100
  zIndex: anchoredMenuZIndex(trigger),
85
101
  };
86
102
  }, [panelId]);
@@ -114,7 +114,9 @@
114
114
  min-width: 0;
115
115
  }
116
116
 
117
- /* 오른쪽 덩이 — 표 번호와 상태 배지. **번호가 앞**이다(배지는 상태, 번호는 이름).
117
+ /* 오른쪽 덩이 — 상태 배지와 표 번호. **번호가 뒤(오른쪽 끝)** 다.
118
+ 한때 번호를 배지보다 앞에 두었다(㊱ 첫 판) — "배지는 상태고 번호는 이름이니 이름이
119
+ 먼저" 라는 셈이었는데, 화면을 보고 사용자가 번호를 맨 끝으로 정했다(2026-10-01).
118
120
  줄어들지 않는다: 좁으면 왼쪽의 등록일이 먼저 말줄임으로 양보한다. */
119
121
  .word-table-button__meta-right {
120
122
  display: flex;
@@ -128,10 +130,17 @@
128
130
  등록일보다 색을 올려 둔다: 둘 다 회색이면 한 줄에 같은 무게로 놓여 훑을 때 안 걸린다.
129
131
  줄이지 않는다 — 번호는 잘리면 뜻이 없다. 좁으면 등록일이 먼저 양보한다. */
130
132
  .word-table-button__code {
131
- font-family: var(--font-mono);
132
- /* 역할 이름을 쓴다 — `--color-black` 은 다크에서 이름과 실제 색이 뒤집히는 '밝기 이름'이다
133
- (tokens.css 가 새 코드는 역할 이름을 쓰라고 적어 두었다). */
134
- color: var(--color-text-primary);
133
+ /* **곁의 글과 같은 글꼴이다 — 고정폭을 쓰지 않는다**(사용자 지시 2026-10-01).
134
+ 한때 `--font-mono` 였다. 까닭은 "종이에 찍힌 번호와 화면의 번호를 눈으로 견줄 때
135
+ 자릿수가 맞으라고" 였는데, 카드 바닥 줄에서 곁의 등록일과 글꼴이 달라 **짧은 번호
136
+ 한 토막이 딴 데서 온 글처럼** 보였다. 그리고 눈으로 견주는 일은 이제 고르개의 찾는
137
+ 칸이 대신한다 — 번호를 치면 그 표만 남는다.
138
+ `caption-1` 이 쥔 글꼴(프리텐다드)을 그대로 받도록 **아무것도 안 적는다.** */
139
+ /* **등록일과 같은 회색이다**(말귀 요청 ㊳, 2026-10-01). 한때 `--color-text-primary` 로
140
+ 한 단 올려 두었다 — "둘 다 회색이면 한 줄에 같은 무게로 놓여 훑을 때 안 걸린다" 는
141
+ 까닭이었는데, 번호가 줄 오른쪽 끝으로 가면서(㊱) 등록일과 떨어져 그 걱정이 없어졌다.
142
+ 자리가 이미 구별을 하므로 색까지 세울 까닭이 없다. */
143
+ color: var(--color-gray);
135
144
  flex-shrink: 0;
136
145
  white-space: nowrap;
137
146
  }
@@ -27,5 +27,5 @@ export default function WordTableButton({ items, name, description, createdAt, c
27
27
  return (_jsxs("div", { className: cx('word-table-button', selected ? 'word-table-button--selected' : '', !hasStoreFooter ? 'word-table-button--no-footer' : '', className), role: "button", tabIndex: 0, "aria-label": code ? `${name} (${code})` : name, onClick: renaming ? undefined : onClick, onKeyDown: handleKey, children: [isNew && _jsx("span", { className: "word-table-button__new-dot", "aria-label": t('myWordTable.newBadge', '새로 만든 표') }), _jsx(WordTableThumbnail, { items: items, className: "word-table-button__thumb" }), _jsxs("div", { className: "word-table-button__info", children: [_jsxs("div", { className: "word-table-button__info-text", children: [titleNode ?? _jsx("div", { className: "word-table-button__title h4", children: name }), description && _jsx("div", { className: "word-table-button__desc body-3", children: description })] }), onMore && !renaming && (_jsx(Button, { htmlType: "button", className: "word-table-button__more", size: 32, type: "dot", variant: "transparent-gray", useBorder: false, icon: "ellipsis",
28
28
  /* 하드코딩 '더보기'였다(영어 화면에서도 한국어로 남았다). 목록에 표가
29
29
  여러 개면 이름이 같은 버튼이 여러 개이기도 해서 이름을 함께 넣는다. */
30
- ariaLabel: t('common.moreFor', { name }), onClick: (e) => { e.stopPropagation(); onMore(e); } }))] }), (code || createdAt || published || shared) && (_jsxs("div", { className: "word-table-button__meta-row", children: [_jsx("span", { className: "word-table-button__meta-left", children: _jsx("span", { className: "word-table-button__meta caption-1", children: createdAt ? formatRelativeTime(createdAt, t) : '' }) }), _jsxs("span", { className: "word-table-button__meta-right", children: [code && _jsx("span", { className: "word-table-button__code caption-1", children: code }), (published || shared) && (_jsxs("span", { className: "word-table-button__badges", children: [published && (_jsx(Badge, { className: "word-table-button__badge", size: "small", variant: "main", fill: "soft", iconLeft: "book-marked", label: publishedLabel ?? t('myWordTable.publishedBadge', '발행됨') })), shared && (_jsx(Badge, { className: "word-table-button__badge", size: "small", fill: "soft", iconLeft: "link", label: sharedLabel ?? t('myWordTable.sharedBadge', '공유 중') }))] }))] })] })), hasStoreFooter && (_jsxs("div", { className: "word-table-button__footer", children: [_jsxs("span", { className: "word-table-button__store-by", children: [publisherName && (_jsx("button", { className: "training-card__publisher body-4", type: "button", onClick: (e) => { e.stopPropagation(); onPublisherClick?.(); }, children: publisherName })), publishedAt && _jsx("span", { className: "training-card__date body-4", children: formatRelativeTime(publishedAt, t) })] }), _jsxs("span", { className: "word-table-button__store-stats", children: [likeCount != null && _jsx(Badge, { size: "small", fill: "soft", variant: "like", iconLeft: "heart", label: String(likeCount) }), downloadCount != null && _jsx(Badge, { size: "small", fill: "soft", variant: "main", iconLeft: "download", label: String(downloadCount) })] })] }))] }));
30
+ ariaLabel: t('common.moreFor', { name }), onClick: (e) => { e.stopPropagation(); onMore(e); } }))] }), (code || createdAt || published || shared) && (_jsxs("div", { className: "word-table-button__meta-row", children: [_jsx("span", { className: "word-table-button__meta-left", children: _jsx("span", { className: "word-table-button__meta caption-1", children: createdAt ? formatRelativeTime(createdAt, t) : '' }) }), _jsxs("span", { className: "word-table-button__meta-right", children: [(published || shared) && (_jsxs("span", { className: "word-table-button__badges", children: [published && (_jsx(Badge, { className: "word-table-button__badge", size: "small", variant: "main", fill: "soft", iconLeft: "book-marked", label: publishedLabel ?? t('myWordTable.publishedBadge', '발행됨') })), shared && (_jsx(Badge, { className: "word-table-button__badge", size: "small", fill: "soft", iconLeft: "link", label: sharedLabel ?? t('myWordTable.sharedBadge', '공유 중') }))] })), code && _jsx("span", { className: "word-table-button__code caption-1", children: code })] })] })), hasStoreFooter && (_jsxs("div", { className: "word-table-button__footer", children: [_jsxs("span", { className: "word-table-button__store-by", children: [publisherName && (_jsx("button", { className: "training-card__publisher body-4", type: "button", onClick: (e) => { e.stopPropagation(); onPublisherClick?.(); }, children: publisherName })), publishedAt && _jsx("span", { className: "training-card__date body-4", children: formatRelativeTime(publishedAt, t) })] }), _jsxs("span", { className: "word-table-button__store-stats", children: [likeCount != null && _jsx(Badge, { size: "small", fill: "soft", variant: "like", iconLeft: "heart", label: String(likeCount) }), downloadCount != null && _jsx(Badge, { size: "small", fill: "soft", variant: "main", iconLeft: "download", label: String(downloadCount) })] })] }))] }));
31
31
  }
@@ -125,7 +125,11 @@
125
125
 
126
126
  띠 높이(36 / 28)와 로고(34 / 24)도 같은 까닭으로 옮겼다. 말귀가 종이로 뽑아 보고
127
127
  `--wt-band-*` 로 덮어 쓰던 값이 그대로 기본이 됐다(사용자 지시 2026-10-01) — 두
128
- 저장소가 같은 수를 두 군데 적고 있을 까닭이 없다. **덮어 쓰던 쪽이 정답이었다.** */
128
+ 저장소가 같은 수를 두 군데 적고 있을 까닭이 없다. **덮어 쓰던 쪽이 정답이었다.**
129
+
130
+ `--wt-brand-logo-height` 는 **34 다.** 한때 말귀가 38 로 올려 쓰며 그 값을 청했는데,
131
+ 사장님이 34 로 못박으셨다(2026-10-01). 36 → 32 → 34 → 38 로 오간 끝의 값이라
132
+ 다시 흔들리기 쉬운 자리다 — 바꾸려면 종이에서 보고 바꾼다. */
129
133
  .word-table__band--top {
130
134
  top: 0;
131
135
  height: var(--wt-band-top-height, 36px);
@@ -206,7 +206,15 @@ export default function WordTable({ title, titleIcon, disc, className, orientati
206
206
  _jsx("div", { className: cx('word-table-cell__score', mark ? 'word-table-cell__score--marked' : ''),
207
207
  /* CSS 의 가운데 맞춤(translateX(-50%))을 그대로 두고 되돌림만 얹는다. 바닥 가운데를
208
208
  기준으로 키워야 칸 아래쪽에 붙은 자리가 흔들리지 않는다. */
209
- style: 채점되돌림 > 1 ? { transform: `translateX(-50%) scale(${채점되돌림})`, transformOrigin: 'bottom center' } : undefined, children: MARKS.map(m => (_jsx("span", { className: cx('word-table-cell__score-slot', mark === m ? 'word-table-cell__score-slot--on' : ''), children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "dot", icon: MARK_ICON[m], variant: mark === m ? MARK_VARIANT[m] : 'default', selected: mark === m, useBorder: false, showTooltipInfo: false, ariaLabel: t(`wordTable.scoring.${m}`, { word: cellLabel, defaultValue: `${cellLabel} ${m}` }), onClick: (e) => { e.stopPropagation(); clickMark(cellIndex, m); } }) }, m))) }));
209
+ style: 채점되돌림 > 1 ? { transform: `translateX(-50%) scale(${채점되돌림})`, transformOrigin: 'bottom center' } : undefined, children: MARKS.map(m => (_jsx("span", { className: cx('word-table-cell__score-slot', mark === m ? 'word-table-cell__score-slot--on' : ''), children: _jsx(Button, { htmlType: "button", size: buttonSize, type: "dot", icon: MARK_ICON[m], variant: mark === m ? MARK_VARIANT[m] : 'default', selected: mark === m, useBorder: false,
210
+ /* **마우스를 올리면 이름이 뜬다**(말귀 요청 ㊴, 2026-10-01). 이 셋은 아이콘만
211
+ 있어 낭독기에는 `낙지 맞음` 으로 제대로 읽히는데 **눈으로 보는 사람에게는
212
+ 이름이 없었다** — 세모가 '애매' 인지 '반만 맞음' 인지 알 길이 없다.
213
+ 띄우는 글에는 **낱말을 안 넣는다**: 낱말은 바로 위 칸에 적혀 있고, 손을
214
+ 얹은 자리에서 알고 싶은 것은 이 표식이 무엇이냐 하나다.
215
+ `ariaLabel` 은 그대로 둔다 — 낭독기는 칸을 안 보고 지나므로 거기서는
216
+ 낱말이 있어야 "어느 칸의 맞음" 인지 알 수 있다. 둘이 일부러 다르다. */
217
+ showTooltipInfo: t(`wordTable.scoring.markName.${m}`, { defaultValue: m }), ariaLabel: t(`wordTable.scoring.${m}`, { word: cellLabel, defaultValue: `${cellLabel} ${m}` }), onClick: (e) => { e.stopPropagation(); clickMark(cellIndex, m); } }) }, m))) }));
210
218
  };
211
219
  const { t } = useTranslation();
212
220
  const isEdit = editCells !== undefined;
@@ -40,6 +40,38 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
40
40
  if (!open)
41
41
  return;
42
42
  recalc();
43
+ /* **판이 선 뒤에 한 번 더 잰다 — 첫 셈은 판 높이를 모른다.**
44
+ 위 `recalc()` 은 `open` 이 참이 된 직후다. 그 시점에 판이 아직 없으면
45
+ `computeStyle` 은 어림값(예: `DatePicker` 의 300)으로 세고, 그 어림값으로
46
+ **위로 펼지 아래로 펼지**와 **창 안으로 조이기**를 둘 다 정해 버린다. 어림이
47
+ 실제보다 작으면 판이 창 밖으로 나가고, 아무도 다시 안 잰다.
48
+ 그리고 판 높이는 그 뒤에도 바뀐다 — 달력은 한 달이 5 줄이냐 6 줄이냐로 한 줄
49
+ (약 40px)이 오르내린다. 높이가 바뀔 때마다 다시 센다.
50
+ **높이가 달라졌을 때만** 다시 센다 — 안 그러면 셈이 자리를 바꾸고 자리가 다시
51
+ 셈을 불러 끝없이 돈다. */
52
+ let 잰높이 = menuRef.current?.offsetHeight ?? 0;
53
+ if (잰높이 > 0)
54
+ recalc();
55
+ /* **오가는 것을 끊는다.** 높이가 달라질 때만 세는 것으로는 두 꼴을 번갈아 오가는 것을
56
+ 못 막는다(A 로 자리를 잡으면 높이가 B 가 되고, B 로 잡으면 다시 A 가 되는 꼴).
57
+ 한 번 여는 동안 몇 번까지만 센다 — 제대로 된 다시 재기는 한두 번이면 끝난다. */
58
+ let 센횟수 = 0;
59
+ let 판관찰 = null;
60
+ if (typeof ResizeObserver !== 'undefined' && menuRef.current) {
61
+ 판관찰 = new ResizeObserver(() => {
62
+ const h = menuRef.current?.offsetHeight ?? 0;
63
+ if (h === 잰높이)
64
+ return;
65
+ 잰높이 = h;
66
+ if (++센횟수 > 4) {
67
+ 판관찰?.disconnect();
68
+ 판관찰 = null;
69
+ return;
70
+ }
71
+ recalc();
72
+ });
73
+ 판관찰.observe(menuRef.current);
74
+ }
43
75
  const handleOutsideClick = (e) => {
44
76
  const target = e.target;
45
77
  if (ignoreOutsideClickSelector && target.closest?.(ignoreOutsideClickSelector))
@@ -83,6 +115,7 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
83
115
  window.addEventListener('orientationchange', handleOrientation);
84
116
  }
85
117
  return () => {
118
+ 판관찰?.disconnect();
86
119
  document.removeEventListener('mousedown', handleOutsideClick);
87
120
  document.removeEventListener('pointerdown', handleOutsideClick);
88
121
  if (onScroll !== 'ignore')
@@ -806,6 +806,11 @@
806
806
  "o": "{{word}} correct",
807
807
  "triangle": "{{word}} partial",
808
808
  "x": "{{word}} incorrect",
809
+ "markName": {
810
+ "o": "Correct",
811
+ "triangle": "Partial",
812
+ "x": "Incorrect"
813
+ },
809
814
  "button": "Score",
810
815
  "pickTitle": "Who are you scoring?",
811
816
  "pickSubtitle": "Choose the customer who trained with “{{name}}”.",
@@ -804,6 +804,11 @@
804
804
  "o": "{{word}} 맞음",
805
805
  "triangle": "{{word}} 애매",
806
806
  "x": "{{word}} 틀림",
807
+ "markName": {
808
+ "o": "맞음",
809
+ "triangle": "애매",
810
+ "x": "틀림"
811
+ },
807
812
  "button": "채점하기",
808
813
  "pickTitle": "누구를 채점할까요?",
809
814
  "pickSubtitle": "‘{{name}}’ 표로 훈련한 고객을 고르세요.",
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.224.0";
49
+ export declare const CORE_VERSION = "0.226.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.224.0';
49
+ export const CORE_VERSION = '0.226.0';