@ihabdevteam/core 0.251.0 → 0.253.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.251.0";
1654
+ export declare const CORE_VERSION = "0.253.0";
1655
1655
 
1656
1656
  /** @public */
1657
1657
  declare const coreDefault: Record<string, unknown>;
@@ -56,10 +56,38 @@ const LOGO_BASE = {
56
56
  manager: 'Logo_speechearManager_kr',
57
57
  };
58
58
  const MAX_TILT = 10;
59
+ /**
60
+ * **글에 적히는 눈금으로 내린 초.**
61
+ *
62
+ * `formatRemaining` 은 가장 큰 단위 하나만 적고 **내린다** — 6.929일은 `6일 남음` 이다.
63
+ * 그런데 % 는 초를 그대로 세고 있어서, 한 카드가 **`6일 남음` 과 `98%` 를 나란히** 세웠다
64
+ * (말귀 라이브 실측 2026-10-02: 7일권 `paused_remaining_seconds = 598706`). 둘 다 맞는데
65
+ * **눈금이 달라 서로를 부정하는 것처럼 보인다** — 눈으로는 6/7 = 86% 인데 고리는 거의 꽉 찼다.
66
+ * 짧은 이용권일수록 벌어진다(3일권이 2.9일 남으면 `2일 남음 · 96%`). 1년짜리는 364/365 라
67
+ * 차이가 안 보여 여태 안 걸렸다.
68
+ *
69
+ * 그래서 **글과 % 가 같은 값을 본다.** 되돌리는 쪽(날을 올려 `7일 남음`)으로는 가지 않는다 —
70
+ * 그건 있지도 않은 정밀도를 부풀리는 쪽이고, **한 번도 안 쓴 새 이용권과 구별이 안 된다.**
71
+ *
72
+ * `formatRemaining` 과 **같은 사다리**를 탄다. 한쪽만 고치면 또 갈리므로 바로 아래 붙여 둔다.
73
+ */
74
+ function 적히는눈금으로(seconds) {
75
+ if (seconds <= 0)
76
+ return 0;
77
+ if (seconds >= 86400)
78
+ return Math.floor(seconds / 86400) * 86400;
79
+ if (seconds >= 3600)
80
+ return Math.floor(seconds / 3600) * 3600;
81
+ if (seconds >= 60)
82
+ return Math.floor(seconds / 60) * 60;
83
+ return Math.floor(seconds);
84
+ }
59
85
  /**
60
86
  * 초 → 가장 큰 단위 하나만("N일 남음" / "N시간 남음" / "N분 남음" / "N초 남음").
61
87
  * 일이 있으면 일까지만, 없으면 시간까지만, 없으면 분까지만, 없으면 초까지.
62
88
  * 하드코딩된 한국어 단위였던 것을 en 로케일에서도 올바르게 보이도록 t()로 번역.
89
+ *
90
+ * **위 `적히는눈금으로` 와 사다리가 같아야 한다** — 이 글과 % 가 같은 값을 보기 때문이다.
63
91
  */
64
92
  function formatRemaining(seconds, t) {
65
93
  if (seconds <= 0)
@@ -161,7 +189,11 @@ export default function PassCard({ ariaLabel, product, showPrice = false, plan,
161
189
  // 정밀도를 반올림으로 부풀리지 않음). "보유 중"인데 아직 한 번도 안 써서 100%
162
190
  // 그대로면 표시하지 않고 카피라이트 유지(진짜 새 이용권이라는 뜻). active/expired는
163
191
  // 100%든 0%든 항상 표시 — 지금 쓰고 있다는 사실 자체가 정보이므로 숨기면 안 됨.
164
- const percent = Math.max(0, Math.min(100, Math.floor((Math.min(remaining, totalSeconds) / totalSeconds) * 100)));
192
+ /* **옆에 적히는 글과 같은 값을 센다**(말귀 요청, 2026-10-02) — 자세한 까닭은
193
+ `적히는눈금으로` 머리글. 날짜(`endDate`)로 셈한 길은 이미 날 단위 정수라 이 걸음이
194
+ 아무것도 바꾸지 않는다. 벌어지던 것은 초를 그대로 받는 `remainingSeconds` 쪽이다. */
195
+ const 적힌남음 = 적히는눈금으로(remaining);
196
+ const percent = Math.max(0, Math.min(100, Math.floor((Math.min(적힌남음, totalSeconds) / totalSeconds) * 100)));
165
197
  if (state === 'pending' && percent >= 100)
166
198
  return null;
167
199
  /* **만료는 '0초 남음' 이 아니다**(말귀 요청 ㊾). "0초 남았다" 는 **곧 끝난다**는 말로
@@ -131,6 +131,33 @@
131
131
  background: var(--color-darkWhite);
132
132
  border-top: 1px solid var(--color-whiteLine);
133
133
  }
134
+ /* **상자 나무에서 빠진다**(말귀 요청 (56), 2026-10-02). 펼친 자리가 `role="row"` 가 되면서
135
+ 안에 `role="cell"` 한 겹이 생겼다 — 그 겹이 보통 상자면 부르는 쪽이 넣은 조각들이 **한
136
+ 덩어리 flex 아이**가 되어 `flex-wrap`·`gap` 이 통째로 죽는다. `display: contents` 면
137
+ 그 겹은 자리를 안 차지하고 아이들이 전처럼 이 줄의 flex 아이로 선다.
138
+ ※ 이 줄을 지우면 펼친 칸의 배치가 무너진다. 역할과 자리 잡기를 한 겹으로 함께 맡은 자리다. */
139
+ .table-row-expand__cell { display: contents; }
140
+
141
+ /* 기다리는 줄·빈 줄의 한 칸 — 여기도 상자 나무에서 빠진다. 줄이 이미 flex 로 가운데를
142
+ 맞추므로, 상자를 가지면 그 겹이 flex 아이가 되어 한 겹 더 생길 뿐이다. */
143
+ .table-row__full-cell { display: contents; }
144
+
145
+ /* **`display: contents` 가 역할을 지우지 않는다** — 실측으로 확인했다(2026-10-02, 놀이터
146
+ `#table`, Chrome). 상자 나무에서 빠지는 마디라 접근성 나무에서도 빠질까 걱정했는데,
147
+ `role` 을 들고 있으면 그대로 선다: `row > cell > generic "Alice 님은 … 가입했습니다."`,
148
+ 기다리는 줄은 `row > cell > status "불러오는 중…"`. 그래서 자리 잡기를 안 건드리고
149
+ 역할만 더할 수 있었다 — 안 그랬으면 flex 설정을 안쪽 겹으로 옮겨야 했다. */
150
+
151
+ /* 줄을 누를 수 있는 표에만 서는 숨은 설명 — 눈에는 없고 `aria-describedby` 로만 읽힌다.
152
+ `display: none` 으로 감추면 **참조해도 안 읽힌다**(㊼ 에서 밟은 자리) — 잘라서 감춘다. */
153
+ .table-row-hint {
154
+ position: absolute;
155
+ width: 1px;
156
+ height: 1px;
157
+ overflow: hidden;
158
+ clip-path: inset(50%);
159
+ white-space: nowrap;
160
+ }
134
161
 
135
162
  /* 높이 스케일 — Panel과 공유. header·fields·row 모두 적용. small 48 / medium 56 / large 64 */
136
163
  .table-wrap--small .table-header,
@@ -12,17 +12,35 @@ import { useTranslation } from 'react-i18next';
12
12
  import './Table.css';
13
13
  import './TableCell.css';
14
14
  import './TableField.css';
15
- import { useState, useRef, useCallback, useEffect } from 'react';
15
+ import { useState, useRef, useCallback, useEffect, useId } from 'react';
16
16
  import ChevronUp from 'lucide-react/dist/esm/icons/chevron-up.mjs';
17
17
  import ChevronDown from 'lucide-react/dist/esm/icons/chevron-down.mjs';
18
18
  import Button from './Button';
19
19
  import Icon from './Icon';
20
20
  import Loader from './Loader';
21
21
  import useMediaQuery from '../hooks/useMediaQuery';
22
+ /* 칸 하나 — `role="cell"` 이라야 낭독기가 **낱낱으로** 읽는다(말귀 요청 (56), 2026-10-02).
23
+ 역할이 없던 때는 줄 전체가 한 덩어리 글로 읽혀 `홍길동 010-1234-5678 2026-09-30 활성`
24
+ 이 한 호흡에 나갔다 — 어디까지가 이름이고 어디부터 전화인지 소리로는 알 수 없었다. */
22
25
  function TableCell({ children, className, style }) {
23
- return _jsx("div", { className: className ? `table-cell ${className}` : 'table-cell', style: style, children: children });
26
+ return _jsx("div", { role: "cell", className: className ? `table-cell ${className}` : 'table-cell', style: style, children: children });
24
27
  }
25
- function TableRow({ variant = 'default', children, className, draggable, onClick, onMouseEnter, onMouseLeave, onDragStart, onDragEnd, expandedContent }) {
28
+ /**
29
+ * 줄 하나 — **`role="row"` 다. 전에는 눌릴 수 있으면 `role="button"` 이었다**(말귀 요청 (56)).
30
+ *
31
+ * `role="button"` 은 두 가지를 망가뜨렸다.
32
+ * 1. 줄이 단추가 되면 안의 칸은 **그 단추의 이름 재료**일 뿐이라 낱낱으로 안 읽힌다.
33
+ * 2. 머리 칸의 `aria-sort` 가 통째로 버려진다 — `columnheader` 는 `row` 안에, 그 `row` 는
34
+ * `table` 안에 있어야 뜻이 생긴다. 줄이 단추면 그 사슬이 끊긴다(눈에는 화살표가
35
+ * 보이므로 오래 안 드러났다).
36
+ *
37
+ * **대신 "눌릴 수 있다" 는 말을 잃는다.** 고객 목록처럼 **줄 자체가 상세로 가는 유일한 길**
38
+ * 인 화면에서는 그 말이 실제로 필요하다(사장님 결정 2026-10-02: 그래도 (가) 로 간다 —
39
+ * 칸을 낱낱이 읽는 쪽이 먼저다). 그래서 `tabIndex`·Enter·Space 는 **그대로 두고**,
40
+ * `aria-describedby` 로 "줄을 누르면 자세히 본다" 를 붙여 메운다. 진짜 단추를 줄 안에
41
+ * 세우는 쪽은 다음 걸음이다 — 그때는 줄 이름을 알려 주는 prop 이 하나 필요하다.
42
+ */
43
+ function TableRow({ variant = 'default', children, className, draggable, onClick, onMouseEnter, onMouseLeave, onDragStart, onDragEnd, expandedContent, colSpan, describedBy }) {
26
44
  const handleKeyDown = useCallback((e) => {
27
45
  if (!onClick)
28
46
  return;
@@ -35,13 +53,18 @@ function TableRow({ variant = 'default', children, className, draggable, onClick
35
53
  onClick();
36
54
  }
37
55
  }, [onClick]);
56
+ /* 기다리는 줄·빈 줄도 **줄이다** — 역할 없는 `div` 를 `rowgroup` 안에 두면 낭독기가
57
+ 건너뛸 수 있어 "없습니다" 가 영 안 들린다. 가로를 다 차지하므로 칸 하나에 `aria-colspan`. */
38
58
  if (variant === 'loading') {
39
- return _jsx("div", { className: "table-row-wrap", children: _jsx("div", { className: "table-row table-row--loading", children: _jsx(Loader, { size: 28 }) }) });
59
+ return (_jsx("div", { role: "presentation", className: "table-row-wrap", children: _jsx("div", { role: "row", className: "table-row table-row--loading", children: _jsx("div", { role: "cell", "aria-colspan": colSpan, className: "table-row__full-cell", children: _jsx(Loader, { size: 28 }) }) }) }));
40
60
  }
41
61
  if (variant === 'empty') {
42
- return _jsx("div", { className: "table-row-wrap", children: _jsx("div", { className: "table-row table-row--empty body-3", children: children }) });
62
+ return (_jsx("div", { role: "presentation", className: "table-row-wrap", children: _jsx("div", { role: "row", className: "table-row table-row--empty body-3", children: _jsx("div", { role: "cell", "aria-colspan": colSpan, className: "table-row__full-cell", children: children }) }) }));
43
63
  }
44
- return (_jsxs("div", { className: "table-row-wrap", children: [_jsx("div", { className: className ? `table-row ${className}` : 'table-row', draggable: draggable, onClick: onClick, onKeyDown: onClick ? handleKeyDown : undefined, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onDragStart: onDragStart, onDragEnd: onDragEnd, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, children: children }), expandedContent && _jsx("div", { className: "table-row-expand", children: expandedContent })] }));
64
+ return (
65
+ /* 겉싸개는 **나무에서 지운다** — `table > row` 사슬 사이에 역할 없는 마디가 끼면
66
+ 그 아래 `row` 가 표의 줄로 안 잡힌다. 자리 잡기(테두리·세로 쌓기)는 그대로다. */
67
+ _jsxs("div", { role: "presentation", className: "table-row-wrap", children: [_jsx("div", { className: className ? `table-row ${className}` : 'table-row', draggable: draggable, onClick: onClick, onKeyDown: onClick ? handleKeyDown : undefined, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onDragStart: onDragStart, onDragEnd: onDragEnd, role: "row", tabIndex: onClick ? 0 : undefined, "aria-describedby": onClick ? describedBy : undefined, children: children }), expandedContent && (_jsx("div", { role: "row", className: "table-row-expand", children: _jsx("div", { role: "cell", "aria-colspan": colSpan, className: "table-row-expand__cell", children: expandedContent }) }))] }));
45
68
  }
46
69
  function SortArrow({ dir }) {
47
70
  const Icon = dir === 'asc' ? ChevronUp : ChevronDown;
@@ -76,6 +99,10 @@ function TableField({ children, onClick, className, sortDir, ariaSort, onResizeS
76
99
  /** @public */
77
100
  export default function Table({ fields = [], headerGroups, items = [], renderRow, onRowClick, isRowClickable, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, mobileGridTemplateColumns, resizable = false, resizeMode = 'grow', resizeStorageKey, size = 'small', title, icon, count, collapsed = false, onToggleCollapse, headerActions, empty, loading, dragOver = false, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }) {
78
101
  const { t } = useTranslation();
102
+ const uid = useId();
103
+ const 제목id = `table-title-${uid}`;
104
+ /** 줄을 눌러 들어가는 표에서만 쓰는 설명 id — `role="button"` 을 잃은 자리를 메운다 */
105
+ const 줄설명id = `table-row-hint-${uid}`;
79
106
  /**
80
107
  * **좁은 화면에서는 폭 조절을 통째로 끈다** — 손잡이·초기화 단추·저장값.
81
108
  *
@@ -462,7 +489,7 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
462
489
  ? { '--table-grid-cols': effectiveGrid }
463
490
  : undefined;
464
491
  const hasHeader = title !== undefined;
465
- return (_jsx("div", { ref: 통달기, className: `table-wrap table-wrap--${size}${폭조절 ? ' table-wrap--resizable' : ''}${dragOver ? ' table-wrap--dragover' : ''}${collapsed ? ' table-wrap--collapsed' : ''}`, style: gridStyle, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop,
492
+ return (_jsxs("div", { ref: 통달기, className: `table-wrap table-wrap--${size}${폭조절 ? ' table-wrap--resizable' : ''}${dragOver ? ' table-wrap--dragover' : ''}${collapsed ? ' table-wrap--collapsed' : ''}`, style: gridStyle, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop,
466
493
  /* **넘칠 때만 초점을 받는다**(말귀 요청 ㊿, 2026-10-01).
467
494
  이 통은 `overflow-x: auto` 인데 초점을 못 받았다. 안에 초점 받을 것이 없으면
468
495
  자판만 쓰는 사람은 **오른쪽 칸을 영영 못 본다** — Tab 이 왼쪽 칸을 벗어나지
@@ -473,30 +500,33 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
473
500
  한 칸씩 는다. 그래서 `scrollWidth > clientWidth` 를 재서 켠다.
474
501
  **이름 있는 `region`** 으로 둔다: 이름 없는 `region` 은 낭독기가 건너뛰고,
475
502
  초점만 받는 `div` 는 "여기서 뭘 하라는 건지" 를 말해 주지 못한다. */
476
- tabIndex: 넘치나 ? 0 : undefined, role: 넘치나 ? 'region' : undefined, "aria-label": 넘치나 ? (typeof title === 'string' ? title : t('table.scrollRegion', '가로로 굴릴 수 있는 표')) : undefined, children: _jsxs("div", { ref: 안달기, className: "table body-2", children: [hasHeader && (_jsxs("div", { className: "table-header", onClick: onToggleCollapse, role: onToggleCollapse ? 'button' : undefined, tabIndex: onToggleCollapse ? 0 : undefined, "aria-expanded": onToggleCollapse ? !collapsed : undefined,
477
- // 머리 안 단추(headerActions)에서 누른 키는 그 단추 몫이다 — 가로채면 표가 접히고 단추 동작은 막혔다(TableRow 와 같게)
478
- onKeyDown: onToggleCollapse ? (e) => { if (e.target !== e.currentTarget)
479
- return; if (e.key === 'Enter' || e.key === ' ') {
480
- e.preventDefault();
481
- onToggleCollapse();
482
- } } : undefined, children: [onToggleCollapse && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), className: "table-header__chevron" })), icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "table-header__icon" })), _jsx("span", { className: "table-header__title h5b", children: title }), count !== undefined && _jsx("span", { className: "table-header__count body-3b", children: count }), headerActions && (_jsx("span", { className: "table-header__actions", onClick: e => e.stopPropagation(), children: headerActions }))] })), !collapsed && (_jsxs(_Fragment, { children: [headerGroups && headerGroups.length > 0 && (_jsx("div", { className: "table-fields table-fields--groups", children: headerGroups.map((g, i) => (_jsx("div", { className: cx('table-field body-3', g.className), style: { gridColumn: `span ${g.span}` }, children: g.label }, i))) })), _jsxs("div", { className: cx('table-fields-strip', 폭조절 && 'table-fields-strip--resizable'), children: [_jsx("div", { className: "table-fields", ref: fieldsRef, children: fields.map((f, i) => (_jsx(TableField, { onClick: f.onClick, className: f.className, sortDir: f.sortDir, ariaSort: ariaSortOf(i),
483
- /* fit 모드는 오른쪽 이웃에서 폭을 덜어 오므로, 덜어 올 컬럼이 없는 자리에는
484
- 손잡이를 두지 않는다 — 두면 그 컬럼만 grow처럼 넓어져 표가 밀린다. */
485
- onResizeStart: 폭조절 && i < fields.length - 1 && f.resizable !== false
486
- && (resizeMode !== 'fit' || fields.some((g, j) => j > i && g.resizable !== false))
487
- ? (e) => startResize(i, e) : undefined, children: f.label }, i))) }), 폭조절 && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: "chevrons-left-right-ellipsis", ariaLabel: t('table.resetColumns', '칸 너비 초기화'), showTooltipInfo: t('table.resetColumns', '칸 너비 초기화'), className: "table-fields__reset", onClick: 자동맞춤 }))] }), _jsx("div", { className: "table-body", children: loading ? (_jsx(TableRow, { variant: "loading" })) : items.length === 0 && empty ? (_jsx(TableRow, { variant: "empty", children: empty })) : items.map((item, idx) => {
488
- const isDragging = draggingId != null && item.id === draggingId;
489
- const 눌림 = !!onRowClick && (isRowClickable ? isRowClickable(item) : true);
490
- /* 글자로 견준다 — URL·폼에서 온 '5' 와 자료의 5 가 같은 줄인데 `===` 로는 안 펴졌다 */
491
- const isExpanded = expandedId != null && item.id != null && (expandedId instanceof Set
492
- ? expandedId.has(item.id) || [...expandedId].some(x => String(x) === String(item.id))
493
- : String(item.id) === String(expandedId));
494
- return (_jsx(TableRow
495
- // 타입까지 열쇠에 — id 없는 줄(idx 0)과 id:0, 1 과 '1' 이 같은 열쇠가 되어 목록을 바꾼 뒤 옛 줄이 남았다
496
- , { className: `${눌림 ? ' table-row--clickable' : ''}${onRowDragStart ? ' table-row--draggable' : ''}${isDragging ? ' table-row--dragging' : ''}`.trim(), draggable: !!onRowDragStart,
497
- // 옆의 핸들러들처럼 있을 때만 넘긴다. 항상 함수를 만들어 넘기면
498
- // TableRow 가 "클릭 가능"으로 보고 role="button" · tabIndex=0 을 붙여,
499
- // 클릭 동작이 없는 표의 모든 행이 버튼으로 읽히고 탭 정지점이 된다.
500
- onClick: 눌림 ? () => onRowClick(item) : undefined, onMouseEnter: onRowMouseEnter ? (e) => onRowMouseEnter(item, e) : undefined, onMouseLeave: onRowMouseLeave ? (e) => onRowMouseLeave(item, e) : undefined, onDragStart: onRowDragStart ? (e) => onRowDragStart(item, e) : undefined, onDragEnd: onRowDragEnd ? (e) => onRowDragEnd(item, e) : undefined, expandedContent: isExpanded && renderExpandedRow ? renderExpandedRow(item) : undefined, children: renderRow(item).map((cell, i) => (_jsx(TableCell, { className: cell.className, style: cell.style, children: cell.content }, i))) }, item.id != null ? `${typeof item.id}:${item.id}` : `__idx:${idx}`));
501
- }) })] }))] }) }));
503
+ tabIndex: 넘치나 ? 0 : undefined, role: 넘치나 ? 'region' : undefined, "aria-label": 넘치나 ? (typeof title === 'string' ? title : t('table.scrollRegion', '가로로 굴릴 수 있는 표')) : undefined, children: [!!onRowClick && _jsx("span", { id: 줄설명id, className: "table-row-hint", children: t('table.rowActionHint', '줄을 누르면 자세히 봅니다') }), _jsxs("div", { ref: 안달기, role: "table", "aria-labelledby": hasHeader && typeof title === 'string' ? 제목id : undefined, className: "table body-2", children: [hasHeader && (_jsxs("div", { className: "table-header", onClick: onToggleCollapse,
504
+ /* 접을 수 없는 머리는 **표의 설명(`caption`)** 이다 — `table` 의 아이로 허락된
505
+ 역할이라 나무가 깨지지 않는다. 접을 수 있으면 그 자체가 단추라 그쪽이 먼저다. */
506
+ role: onToggleCollapse ? 'button' : 'caption', tabIndex: onToggleCollapse ? 0 : undefined, "aria-expanded": onToggleCollapse ? !collapsed : undefined,
507
+ // 머리 안 단추(headerActions)에서 누른 키는 그 단추 몫이다 — 가로채면 표가 접히고 단추 동작은 막혔다(TableRow 와 같게)
508
+ onKeyDown: onToggleCollapse ? (e) => { if (e.target !== e.currentTarget)
509
+ return; if (e.key === 'Enter' || e.key === ' ') {
510
+ e.preventDefault();
511
+ onToggleCollapse();
512
+ } } : undefined, children: [onToggleCollapse && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-right' : 'chevron-down', ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), className: "table-header__chevron" })), icon && (_jsx(Icon, { icon: icon, label: typeof title === 'string' ? title : undefined, className: "table-header__icon" })), _jsx("span", { id: 제목id, className: "table-header__title h5b", children: title }), count !== undefined && _jsx("span", { className: "table-header__count body-3b", children: count }), headerActions && (_jsx("span", { className: "table-header__actions", onClick: e => e.stopPropagation(), children: headerActions }))] })), !collapsed && (_jsxs(_Fragment, { children: [headerGroups && headerGroups.length > 0 && (_jsx("div", { role: "row", className: "table-fields table-fields--groups", children: headerGroups.map((g, i) => (_jsx("div", { role: "columnheader", "aria-colspan": g.span, className: cx('table-field body-3', g.className), style: { gridColumn: `span ${g.span}` }, children: g.label }, i))) })), _jsxs("div", { role: "presentation", className: cx('table-fields-strip', 폭조절 && 'table-fields-strip--resizable'), children: [_jsx("div", { role: "row", className: "table-fields", ref: fieldsRef, children: fields.map((f, i) => (_jsx(TableField, { onClick: f.onClick, className: f.className, sortDir: f.sortDir, ariaSort: ariaSortOf(i),
513
+ /* fit 모드는 오른쪽 이웃에서 폭을 덜어 오므로, 덜어 올 컬럼이 없는 자리에는
514
+ 손잡이를 두지 않는다 — 두면 그 컬럼만 grow처럼 넓어져 표가 밀린다. */
515
+ onResizeStart: 폭조절 && i < fields.length - 1 && f.resizable !== false
516
+ && (resizeMode !== 'fit' || fields.some((g, j) => j > i && g.resizable !== false))
517
+ ? (e) => startResize(i, e) : undefined, children: f.label }, i))) }), 폭조절 && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: "chevrons-left-right-ellipsis", ariaLabel: t('table.resetColumns', '칸 너비 초기화'), showTooltipInfo: t('table.resetColumns', '칸 너비 초기화'), className: "table-fields__reset", onClick: 자동맞춤 }))] }), _jsx("div", { role: "rowgroup", className: "table-body", children: loading ? (_jsx(TableRow, { variant: "loading", colSpan: fields.length || undefined })) : items.length === 0 && empty ? (_jsx(TableRow, { variant: "empty", colSpan: fields.length || undefined, children: empty })) : items.map((item, idx) => {
518
+ const isDragging = draggingId != null && item.id === draggingId;
519
+ const 눌림 = !!onRowClick && (isRowClickable ? isRowClickable(item) : true);
520
+ /* 글자로 견준다 — URL·폼에서 온 '5' 와 자료의 5 가 같은 줄인데 `===` 로는 안 펴졌다 */
521
+ const isExpanded = expandedId != null && item.id != null && (expandedId instanceof Set
522
+ ? expandedId.has(item.id) || [...expandedId].some(x => String(x) === String(item.id))
523
+ : String(item.id) === String(expandedId));
524
+ return (_jsx(TableRow
525
+ // 타입까지 열쇠에 — id 없는 줄(idx 0)과 id:0, 1 과 '1' 이 같은 열쇠가 되어 목록을 바꾼 뒤 옛 줄이 남았다
526
+ , { className: `${눌림 ? ' table-row--clickable' : ''}${onRowDragStart ? ' table-row--draggable' : ''}${isDragging ? ' table-row--dragging' : ''}`.trim(), draggable: !!onRowDragStart,
527
+ // 옆의 핸들러들처럼 있을 때만 넘긴다. 항상 함수를 만들어 넘기면
528
+ // TableRow 가 "클릭 가능"으로 보고 role="button" · tabIndex=0 을 붙여,
529
+ // 클릭 동작이 없는 표의 모든 행이 버튼으로 읽히고 탭 정지점이 된다.
530
+ onClick: 눌림 ? () => onRowClick(item) : undefined, colSpan: fields.length || undefined, describedBy: 줄설명id, onMouseEnter: onRowMouseEnter ? (e) => onRowMouseEnter(item, e) : undefined, onMouseLeave: onRowMouseLeave ? (e) => onRowMouseLeave(item, e) : undefined, onDragStart: onRowDragStart ? (e) => onRowDragStart(item, e) : undefined, onDragEnd: onRowDragEnd ? (e) => onRowDragEnd(item, e) : undefined, expandedContent: isExpanded && renderExpandedRow ? renderExpandedRow(item) : undefined, children: renderRow(item).map((cell, i) => (_jsx(TableCell, { className: cell.className, style: cell.style, children: cell.content }, i))) }, item.id != null ? `${typeof item.id}:${item.id}` : `__idx:${idx}`));
531
+ }) })] }))] })] }));
502
532
  }
@@ -1404,7 +1404,8 @@
1404
1404
  },
1405
1405
  "table": {
1406
1406
  "resetColumns": "Reset column widths",
1407
- "scrollRegion": "Horizontally scrollable table"
1407
+ "scrollRegion": "Horizontally scrollable table",
1408
+ "rowActionHint": "Press a row to open its details"
1408
1409
  },
1409
1410
  "wordScore": {
1410
1411
  "panelTitle": "Word table scores",
@@ -1402,7 +1402,8 @@
1402
1402
  },
1403
1403
  "table": {
1404
1404
  "resetColumns": "칸 너비 초기화",
1405
- "scrollRegion": "가로로 굴릴 수 있는 표"
1405
+ "scrollRegion": "가로로 굴릴 수 있는 표",
1406
+ "rowActionHint": "줄을 누르면 자세히 봅니다"
1406
1407
  },
1407
1408
  "wordScore": {
1408
1409
  "panelTitle": "단어표 채점",
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.251.0";
49
+ export declare const CORE_VERSION = "0.253.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.251.0';
49
+ export const CORE_VERSION = '0.253.0';