@ihabdevteam/core 0.252.0 → 0.254.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.252.0";
1654
+ export declare const CORE_VERSION = "0.254.0";
1655
1655
 
1656
1656
  /** @public */
1657
1657
  declare const coreDefault: Record<string, unknown>;
@@ -14,12 +14,14 @@
14
14
  text-align: left;
15
15
  }
16
16
 
17
- .table-header[role="button"] {
17
+ /* 전에는 `[role="button"]` 으로 잡았다 — 머리가 단추 역할을 내놓으면서(단추 안 단추를
18
+ 걷었다, 2026-10-02) 걸 자리가 없어졌다. `.table-row--clickable` 과 같은 결의 클래스로 옮긴다. */
19
+ .table-header--clickable {
18
20
  cursor: pointer;
19
21
  transition: background 0.15s;
20
22
  }
21
23
 
22
- .table-header[role="button"]:hover {
24
+ .table-header--clickable:hover {
23
25
  background: var(--color-darkWhite);
24
26
  }
25
27
 
@@ -131,6 +133,33 @@
131
133
  background: var(--color-darkWhite);
132
134
  border-top: 1px solid var(--color-whiteLine);
133
135
  }
136
+ /* **상자 나무에서 빠진다**(말귀 요청 (56), 2026-10-02). 펼친 자리가 `role="row"` 가 되면서
137
+ 안에 `role="cell"` 한 겹이 생겼다 — 그 겹이 보통 상자면 부르는 쪽이 넣은 조각들이 **한
138
+ 덩어리 flex 아이**가 되어 `flex-wrap`·`gap` 이 통째로 죽는다. `display: contents` 면
139
+ 그 겹은 자리를 안 차지하고 아이들이 전처럼 이 줄의 flex 아이로 선다.
140
+ ※ 이 줄을 지우면 펼친 칸의 배치가 무너진다. 역할과 자리 잡기를 한 겹으로 함께 맡은 자리다. */
141
+ .table-row-expand__cell { display: contents; }
142
+
143
+ /* 기다리는 줄·빈 줄의 한 칸 — 여기도 상자 나무에서 빠진다. 줄이 이미 flex 로 가운데를
144
+ 맞추므로, 상자를 가지면 그 겹이 flex 아이가 되어 한 겹 더 생길 뿐이다. */
145
+ .table-row__full-cell { display: contents; }
146
+
147
+ /* **`display: contents` 가 역할을 지우지 않는다** — 실측으로 확인했다(2026-10-02, 놀이터
148
+ `#table`, Chrome). 상자 나무에서 빠지는 마디라 접근성 나무에서도 빠질까 걱정했는데,
149
+ `role` 을 들고 있으면 그대로 선다: `row > cell > generic "Alice 님은 … 가입했습니다."`,
150
+ 기다리는 줄은 `row > cell > status "불러오는 중…"`. 그래서 자리 잡기를 안 건드리고
151
+ 역할만 더할 수 있었다 — 안 그랬으면 flex 설정을 안쪽 겹으로 옮겨야 했다. */
152
+
153
+ /* 줄을 누를 수 있는 표에만 서는 숨은 설명 — 눈에는 없고 `aria-describedby` 로만 읽힌다.
154
+ `display: none` 으로 감추면 **참조해도 안 읽힌다**(㊼ 에서 밟은 자리) — 잘라서 감춘다. */
155
+ .table-row-hint {
156
+ position: absolute;
157
+ width: 1px;
158
+ height: 1px;
159
+ overflow: hidden;
160
+ clip-path: inset(50%);
161
+ white-space: nowrap;
162
+ }
134
163
 
135
164
  /* 높이 스케일 — Panel과 공유. header·fields·row 모두 적용. small 48 / medium 56 / large 64 */
136
165
  .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,46 @@ 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: cx('table-header', onToggleCollapse && 'table-header--clickable'), onClick: onToggleCollapse,
504
+ /**
505
+ * **머리는 늘 `caption` 이다 — 단추가 아니다**(사장님 지시 2026-10-02).
506
+ *
507
+ * 전에는 접을 수 있으면 이 `div` 가 `role="button"` 에 `tabIndex=0` 이었다. 그런데
508
+ * **그 안에 이미 진짜 `<button>`("접기"/"펼치기")이 있다** — 단추 안 단추이고,
509
+ * 한 가지 일에 **탭 정지점이 둘**이었다(자판으로 도는 사람은 같은 '접기' 를 두 번
510
+ * 잡는다). 게다가 `.table` 이 `role="table"` 이 된 뒤로는 그 단추가 나무에서
511
+ * **표의 바로 아래 아이**로 서서 표의 꼴까지 흐렸다.
512
+ *
513
+ * 그래서 역할·정지점·키 처리를 **안쪽 진짜 단추에 맡긴다.** 여닫힘(`aria-expanded`)도
514
+ * 그 단추로 옮겼다 — 여는 것이 그것이니 상태도 그것이 말해야 한다.
515
+ *
516
+ * **머리 아무 데나 눌러 접는 손맛은 그대로다** — `onClick` 이 그대로 있고, 안쪽
517
+ * 단추가 낸 클릭도 여기로 올라와 한 번만 불린다. 손 모양·hover 는 CSS 가
518
+ * `.table-header--clickable` 로 잡는다(전에는 `[role="button"]` 로 잡았다).
519
+ *
520
+ * 손으로 붙이던 `onKeyDown` 은 **지웠다** — 진짜 단추라 Enter·Space 는 브라우저가
521
+ * 처리하고, "머리 안 단추에서 누른 키는 그 단추 몫" 이라는 가드도 저절로 선다.
522
+ */
523
+ role: "caption", 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', '접기'),
524
+ /* 여닫힘은 **여는 것**이 말한다 — 전에는 겉의 `div` 에 달려 있었다(위 머리글) */
525
+ ariaExpanded: !collapsed, 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),
526
+ /* fit 모드는 오른쪽 이웃에서 폭을 덜어 오므로, 덜어 올 컬럼이 없는 자리에는
527
+ 손잡이를 두지 않는다 — 두면 그 컬럼만 grow처럼 넓어져 표가 밀린다. */
528
+ onResizeStart: 폭조절 && i < fields.length - 1 && f.resizable !== false
529
+ && (resizeMode !== 'fit' || fields.some((g, j) => j > i && g.resizable !== false))
530
+ ? (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) => {
531
+ const isDragging = draggingId != null && item.id === draggingId;
532
+ const 눌림 = !!onRowClick && (isRowClickable ? isRowClickable(item) : true);
533
+ /* 글자로 견준다 — URL·폼에서 온 '5' 와 자료의 5 가 같은 줄인데 `===` 로는 안 펴졌다 */
534
+ const isExpanded = expandedId != null && item.id != null && (expandedId instanceof Set
535
+ ? expandedId.has(item.id) || [...expandedId].some(x => String(x) === String(item.id))
536
+ : String(item.id) === String(expandedId));
537
+ return (_jsx(TableRow
538
+ // 타입까지 열쇠에 — id 없는 줄(idx 0)과 id:0, 1 과 '1' 이 같은 열쇠가 되어 목록을 바꾼 뒤 옛 줄이 남았다
539
+ , { className: `${눌림 ? ' table-row--clickable' : ''}${onRowDragStart ? ' table-row--draggable' : ''}${isDragging ? ' table-row--dragging' : ''}`.trim(), draggable: !!onRowDragStart,
540
+ // 옆의 핸들러들처럼 있을 때만 넘긴다. 항상 함수를 만들어 넘기면
541
+ // TableRow 가 "클릭 가능"으로 보고 role="button" · tabIndex=0 을 붙여,
542
+ // 클릭 동작이 없는 표의 모든 행이 버튼으로 읽히고 탭 정지점이 된다.
543
+ 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}`));
544
+ }) })] }))] })] }));
502
545
  }
@@ -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.252.0";
49
+ export declare const CORE_VERSION = "0.254.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.252.0';
49
+ export const CORE_VERSION = '0.254.0';