@ihabdevteam/core 0.223.0 → 0.225.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 +65 -0
- package/dist/components.cjs.js +46 -46
- 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 +16 -16
- package/dist/ihabdevteam-core.d.ts +12 -1
- package/dist/packages/core/src/components/DatePicker.js +7 -1
- package/dist/packages/core/src/components/Toolbar.d.ts +11 -0
- package/dist/packages/core/src/components/Toolbar.js +1 -1
- package/dist/packages/core/src/components/Word/WordTableButton.css +8 -4
- package/dist/packages/core/src/components/Word/WordTableButton.js +1 -1
- package/dist/packages/core/src/components/WordTable.js +9 -1
- package/dist/packages/core/src/hooks/useAnchoredMenu.js +24 -0
- package/dist/packages/core/src/i18n/locales/en.json +5 -0
- package/dist/packages/core/src/i18n/locales/ko.json +5 -0
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
1629
|
+
export declare const CORE_VERSION = "0.225.0";
|
|
1630
1630
|
|
|
1631
1631
|
/** @public */
|
|
1632
1632
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -7811,10 +7811,21 @@ export declare interface ToolbarDropdown {
|
|
|
7811
7811
|
|
|
7812
7812
|
export declare interface ToolbarProps {
|
|
7813
7813
|
count?: ToolbarCount;
|
|
7814
|
+
/** 오른쪽 그룹 맨 앞의 검색 칸.
|
|
7815
|
+
*
|
|
7816
|
+
* `autoFocus` 는 **칸 하나를 채우러 여는 창**을 위한 것이다(말귀 요청 ㊲, 2026-10-01).
|
|
7817
|
+
* 그런 창은 열자마자 칠 수 있어야 하는데, 초점이 `닫기` 단추에 있으면 칸을 한 번
|
|
7818
|
+
* 눌러야 한다 — 그 한 걸음이 "나중에 하지" 가 되어, 창을 지은 까닭 자체가 사라진다
|
|
7819
|
+
* (말귀에서 번호 치는 칸을 이 `search` 로 옮기며 실제로 그렇게 됐다).
|
|
7820
|
+
*
|
|
7821
|
+
* **화면을 여는 것만으로 초점이 옮겨 가는 자리에는 주지 않는다** — 목록 화면의
|
|
7822
|
+
* 거르개처럼 사람이 읽으러 온 곳에서 초점이 검색 칸으로 뛰면, 화면낭독기는 머리글을
|
|
7823
|
+
* 건너뛰고 폰은 자판이 올라와 화면 절반을 덮는다. 그래서 기본은 거짓이다. */
|
|
7814
7824
|
search?: {
|
|
7815
7825
|
value: string;
|
|
7816
7826
|
onChange: (v: string) => void;
|
|
7817
7827
|
placeholder?: string;
|
|
7828
|
+
autoFocus?: boolean;
|
|
7818
7829
|
};
|
|
7819
7830
|
/** 1번째 row 왼쪽 — 탭 필터 세그먼트.
|
|
7820
7831
|
* **여럿을 줄 수 있다.** 고르개가 둘인 화면(먼저 무엇을 볼지 고르고, 그 안에서
|
|
@@ -68,7 +68,13 @@ export default function DatePicker({ value, onChange, size = 48, min, max, disab
|
|
|
68
68
|
const dropdownH = 판?.offsetHeight || 300;
|
|
69
69
|
const 판폭 = Math.max(rect.width, 판?.offsetWidth || 0, 260);
|
|
70
70
|
const spaceBelow = vh - rect.bottom;
|
|
71
|
-
const
|
|
71
|
+
const spaceAbove = rect.top;
|
|
72
|
+
/* **아래가 모자라면 넓은 쪽으로 편다**(말귀 요청 ㊵, 2026-10-01).
|
|
73
|
+
한때는 `rect.top > dropdownH`, 곧 **위가 통째로 남을 때만** 뒤집었다. 그래서 위도
|
|
74
|
+
아래도 다 모자라면 늘 아래로 폈고 — 칸이 창 바닥 줄에 있으면 거의 늘 그 꼴이다 —
|
|
75
|
+
아래로 삐져나갔다. 모자라는 것이 정해졌다면 **덜 모자라는 쪽**이 낫다.
|
|
76
|
+
`6` 과 `여백` 은 아래 자리 잡기와 같은 값이다. */
|
|
77
|
+
const above = spaceBelow < dropdownH && spaceAbove > spaceBelow;
|
|
72
78
|
/* **창 안으로 조인다** — 스크롤을 따라 다시 잴 때 트리거가 화면 밖으로 나가면 달력이 창 위·아래 밖까지 따라가
|
|
73
79
|
고를 수 없었다. 판이 창보다 크면 위 여백에 붙인다. */
|
|
74
80
|
const 여백 = 8;
|
|
@@ -24,10 +24,21 @@ export interface ToolbarCount {
|
|
|
24
24
|
}
|
|
25
25
|
export interface ToolbarProps {
|
|
26
26
|
count?: ToolbarCount;
|
|
27
|
+
/** 오른쪽 그룹 맨 앞의 검색 칸.
|
|
28
|
+
*
|
|
29
|
+
* `autoFocus` 는 **칸 하나를 채우러 여는 창**을 위한 것이다(말귀 요청 ㊲, 2026-10-01).
|
|
30
|
+
* 그런 창은 열자마자 칠 수 있어야 하는데, 초점이 `닫기` 단추에 있으면 칸을 한 번
|
|
31
|
+
* 눌러야 한다 — 그 한 걸음이 "나중에 하지" 가 되어, 창을 지은 까닭 자체가 사라진다
|
|
32
|
+
* (말귀에서 번호 치는 칸을 이 `search` 로 옮기며 실제로 그렇게 됐다).
|
|
33
|
+
*
|
|
34
|
+
* **화면을 여는 것만으로 초점이 옮겨 가는 자리에는 주지 않는다** — 목록 화면의
|
|
35
|
+
* 거르개처럼 사람이 읽으러 온 곳에서 초점이 검색 칸으로 뛰면, 화면낭독기는 머리글을
|
|
36
|
+
* 건너뛰고 폰은 자판이 올라와 화면 절반을 덮는다. 그래서 기본은 거짓이다. */
|
|
27
37
|
search?: {
|
|
28
38
|
value: string;
|
|
29
39
|
onChange: (v: string) => void;
|
|
30
40
|
placeholder?: string;
|
|
41
|
+
autoFocus?: boolean;
|
|
31
42
|
};
|
|
32
43
|
/** 1번째 row 왼쪽 — 탭 필터 세그먼트.
|
|
33
44
|
* **여럿을 줄 수 있다.** 고르개가 둘인 화면(먼저 무엇을 볼지 고르고, 그 안에서
|
|
@@ -18,5 +18,5 @@ export default function Toolbar({ count, search, segment, filters, pageSize, pag
|
|
|
18
18
|
/* placeholder는 접근 가능한 이름이 아니다 — 입력이 시작되면 사라지고
|
|
19
19
|
화면낭독기 지원도 제각각이라, 같은 문구를 aria-label로도 준다.
|
|
20
20
|
(툴바 검색은 시각적 라벨이 따로 없는 자리라 이게 유일한 이름이 된다.) */
|
|
21
|
-
ariaLabel: search.placeholder ?? t('common.search', '검색'), clearable: true })), filters?.map((f, i) => (_jsx(Dropdown, { size: 40, options: f.options, value: f.value, onChange: f.onChange, ariaLabel: f.ariaLabel }, i))), (pageSize || pagination) && (_jsxs("div", { className: "toolbar__pager", children: [pageSize && _jsx(Dropdown, { size: 40, options: pageSize.options, value: pageSize.value, onChange: pageSize.onChange, ariaLabel: pageSize.ariaLabel ?? t('common.pageSizeSelect', '페이지당 표시 개수') }), pagination && (_jsx(Pagination, { page: pagination.page, totalPages: pagination.total, onChange: pagination.onChange, prevLabel: pagination.prevLabel, nextLabel: pagination.nextLabel }))] })), actions] }) }))] }));
|
|
21
|
+
ariaLabel: search.placeholder ?? t('common.search', '검색'), autoFocus: search.autoFocus, clearable: true })), filters?.map((f, i) => (_jsx(Dropdown, { size: 40, options: f.options, value: f.value, onChange: f.onChange, ariaLabel: f.ariaLabel }, i))), (pageSize || pagination) && (_jsxs("div", { className: "toolbar__pager", children: [pageSize && _jsx(Dropdown, { size: 40, options: pageSize.options, value: pageSize.value, onChange: pageSize.onChange, ariaLabel: pageSize.ariaLabel ?? t('common.pageSizeSelect', '페이지당 표시 개수') }), pagination && (_jsx(Pagination, { page: pagination.page, totalPages: pagination.total, onChange: pagination.onChange, prevLabel: pagination.prevLabel, nextLabel: pagination.nextLabel }))] })), actions] }) }))] }));
|
|
22
22
|
}
|
|
@@ -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;
|
|
@@ -129,9 +131,11 @@
|
|
|
129
131
|
줄이지 않는다 — 번호는 잘리면 뜻이 없다. 좁으면 등록일이 먼저 양보한다. */
|
|
130
132
|
.word-table-button__code {
|
|
131
133
|
font-family: var(--font-mono);
|
|
132
|
-
/*
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
/* **등록일과 같은 회색이다**(말귀 요청 ㊳, 2026-10-01). 한때 `--color-text-primary` 로
|
|
135
|
+
한 단 올려 두었다 — "둘 다 회색이면 한 줄에 같은 무게로 놓여 훑을 때 안 걸린다" 는
|
|
136
|
+
까닭이었는데, 번호가 줄 오른쪽 끝으로 가면서(㊱) 등록일과 떨어져 그 걱정이 없어졌다.
|
|
137
|
+
자리가 이미 구별을 하므로 색까지 세울 까닭이 없다. */
|
|
138
|
+
color: var(--color-gray);
|
|
135
139
|
flex-shrink: 0;
|
|
136
140
|
white-space: nowrap;
|
|
137
141
|
}
|
|
@@ -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: [
|
|
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
|
}
|
|
@@ -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,
|
|
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,29 @@ 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
|
+
let 판관찰 = null;
|
|
56
|
+
if (typeof ResizeObserver !== 'undefined' && menuRef.current) {
|
|
57
|
+
판관찰 = new ResizeObserver(() => {
|
|
58
|
+
const h = menuRef.current?.offsetHeight ?? 0;
|
|
59
|
+
if (h === 잰높이)
|
|
60
|
+
return;
|
|
61
|
+
잰높이 = h;
|
|
62
|
+
recalc();
|
|
63
|
+
});
|
|
64
|
+
판관찰.observe(menuRef.current);
|
|
65
|
+
}
|
|
43
66
|
const handleOutsideClick = (e) => {
|
|
44
67
|
const target = e.target;
|
|
45
68
|
if (ignoreOutsideClickSelector && target.closest?.(ignoreOutsideClickSelector))
|
|
@@ -83,6 +106,7 @@ export function useAnchoredMenu({ computeStyle, onScroll = 'close', onResize = '
|
|
|
83
106
|
window.addEventListener('orientationchange', handleOrientation);
|
|
84
107
|
}
|
|
85
108
|
return () => {
|
|
109
|
+
판관찰?.disconnect();
|
|
86
110
|
document.removeEventListener('mousedown', handleOutsideClick);
|
|
87
111
|
document.removeEventListener('pointerdown', handleOutsideClick);
|
|
88
112
|
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}}’ 표로 훈련한 고객을 고르세요.",
|