@ihabdevteam/core 0.131.0 → 0.132.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/components.cjs.js +177 -177
  3. package/dist/components.esm.css +2 -2
  4. package/dist/hooks.cjs.js +1 -1
  5. package/dist/i18n.cjs.js +1 -1
  6. package/dist/ihabdevteam-core.cjs.js +182 -182
  7. package/dist/ihabdevteam-core.d.ts +55 -6
  8. package/dist/packages/core/src/components/Accordion.js +4 -1
  9. package/dist/packages/core/src/components/Badge.d.ts +5 -0
  10. package/dist/packages/core/src/components/Badge.js +3 -3
  11. package/dist/packages/core/src/components/BarChart.d.ts +1 -1
  12. package/dist/packages/core/src/components/BarChart.js +24 -13
  13. package/dist/packages/core/src/components/Calendar.js +4 -0
  14. package/dist/packages/core/src/components/ChartSection.css +16 -0
  15. package/dist/packages/core/src/components/ChartSection.d.ts +8 -1
  16. package/dist/packages/core/src/components/ChartSection.js +2 -2
  17. package/dist/packages/core/src/components/PageTitle.css +6 -0
  18. package/dist/packages/core/src/components/PageTitle.d.ts +9 -1
  19. package/dist/packages/core/src/components/PageTitle.js +2 -2
  20. package/dist/packages/core/src/components/Popup/Popup.d.ts +8 -0
  21. package/dist/packages/core/src/components/Popup/Popup.js +4 -2
  22. package/dist/packages/core/src/components/RingChart.d.ts +3 -1
  23. package/dist/packages/core/src/components/RingChart.js +2 -2
  24. package/dist/packages/core/src/components/SideNav.js +27 -10
  25. package/dist/packages/core/src/components/Table.d.ts +17 -1
  26. package/dist/packages/core/src/components/Table.js +15 -9
  27. package/dist/packages/core/src/components/TextField.js +34 -5
  28. package/dist/packages/core/src/components/barChartTypes.d.ts +2 -0
  29. package/dist/packages/core/src/components/useComboboxSuggestions.d.ts +1 -0
  30. package/dist/packages/core/src/components/useComboboxSuggestions.js +12 -6
  31. package/dist/packages/core/src/version.d.ts +1 -1
  32. package/dist/packages/core/src/version.js +1 -1
  33. package/dist/utils.cjs.js +1 -1
  34. package/package.json +1 -1
@@ -40,12 +40,28 @@ export type TableProps<T extends {
40
40
  style?: CSSProperties;
41
41
  }>;
42
42
  onRowClick?: (item: T) => void;
43
+ /**
44
+ * 줄마다 누를 수 있는지. false 인 줄은 `onRowClick` 이 안 불리고, 버튼으로 읽히지도(role·Tab 정지점) 않으며,
45
+ * 손가락 커서·hover 강조도 없다. `onRowClick` 이 있을 때만 뜻이 있다. 미지정이면 모든 줄이 눌린다(기본).
46
+ *
47
+ * 예: 방문 기록 표에서 '안 옴' 줄은 열 것이 없다 — CSS 로 커서만 되돌리면 눌리기는 그대로이고 낭독기는 여전히 버튼이라 한다.
48
+ */
49
+ isRowClickable?: (item: T) => boolean;
43
50
  onRowMouseEnter?: (item: T, event: MouseEvent<HTMLDivElement>) => void;
44
51
  onRowMouseLeave?: (item: T, event: MouseEvent<HTMLDivElement>) => void;
45
52
  onRowDragStart?: (item: T, event: DragEvent<HTMLDivElement>) => void;
46
53
  onRowDragEnd?: (item: T, event: DragEvent<HTMLDivElement>) => void;
47
54
  draggingId?: string | number | null;
48
55
  gridTemplateColumns?: string;
56
+ /**
57
+ * 좁은 화면(767px 이하)에서 쓸 열 틀. 주면 그 폭에서는 이 틀을 쓰고 **끌어 둔 폭(저장값)은 무시**한다 —
58
+ * 폭 조절 손잡이와 '칸 너비 초기화' 단추도 숨겨, 좁은 화면에서 끈 값이 넓은 화면의 저장값을 덮지 않게 한다.
59
+ * 넓은 화면으로 돌아오면 저장값이 그대로 돌아온다. 미지정이면 좁은 화면도 넓은 화면과 같다(기본).
60
+ *
61
+ * 열 틀은 인라인 CSS 변수로 들어가므로 앱 CSS 로는 `!important` 없이 못 이긴다 — 그래서 prop 이다.
62
+ * 숨길 칸은 `renderRow` 의 `className`·`fields[].className` 으로 앱이 가린다.
63
+ */
64
+ mobileGridTemplateColumns?: string;
49
65
  /** true면 헤더 경계선을 드래그해 컬럼 폭 조절 가능 */
50
66
  resizable?: boolean;
51
67
  /**
@@ -98,4 +114,4 @@ export type TableProps<T extends {
98
114
  /** @public */
99
115
  export default function Table<T extends {
100
116
  id?: string | number;
101
- }>({ fields, headerGroups, items, renderRow, onRowClick, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, resizable, resizeMode, resizeStorageKey, size, title, icon, count, collapsed, onToggleCollapse, headerActions, empty, loading, dragOver, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }: TableProps<T>): import("react").JSX.Element;
117
+ }>({ fields, headerGroups, items, renderRow, onRowClick, isRowClickable, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, mobileGridTemplateColumns, resizable, resizeMode, resizeStorageKey, size, title, icon, count, collapsed, onToggleCollapse, headerActions, empty, loading, dragOver, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }: TableProps<T>): import("react").JSX.Element;
@@ -18,6 +18,7 @@ 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
+ import useMediaQuery from '../hooks/useMediaQuery';
21
22
  function TableCell({ children, className, style }) {
22
23
  return _jsx("div", { className: className ? `table-cell ${className}` : 'table-cell', style: style, children: children });
23
24
  }
@@ -62,8 +63,12 @@ function TableField({ children, onClick, className, sortDir, onResizeStart }) {
62
63
  return (_jsxs("div", { onClick: onClick, onKeyDown: onClick ? handleKeyDown : undefined, role: onClick ? 'button' : undefined, tabIndex: onClick ? 0 : undefined, "aria-sort": sortDir ? (sortDir === 'asc' ? 'ascending' : 'descending') : undefined, className: cx('table-field body-3', className, sortDir ? 'table-field--sorted' : '', onResizeStart ? 'table-field--resizable' : ''), children: [children, sortDir && _jsx(SortArrow, { dir: sortDir }), onResizeStart && (_jsx("span", { className: "table-field__resizer", onPointerDown: onResizeStart, onClick: e => e.stopPropagation(), "aria-hidden": "true" }))] }));
63
64
  }
64
65
  /** @public */
65
- export default function Table({ fields = [], headerGroups, items = [], renderRow, onRowClick, onRowMouseEnter, onRowMouseLeave, onRowDragStart, onRowDragEnd, draggingId, gridTemplateColumns, resizable = false, resizeMode = 'grow', resizeStorageKey, size = 'small', title, icon, count, collapsed = false, onToggleCollapse, headerActions, empty, loading, dragOver = false, onDragOver, onDragLeave, onDrop, renderExpandedRow, expandedId, }) {
66
+ 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, }) {
66
67
  const { t } = useTranslation();
68
+ /* 좁은 화면 틀이 걸리면 폭 조절을 통째로 끈다 — 손잡이·초기화 단추·저장. 훅은 늘 부른다(순서 고정). */
69
+ const 좁은화면 = useMediaQuery('(max-width: 767px)');
70
+ const 좁은틀 = mobileGridTemplateColumns && 좁은화면 ? mobileGridTemplateColumns : null;
71
+ const 폭조절 = resizable && !좁은틀;
67
72
  // ── 컬럼 폭 드래그 리사이즈 (resizable) ──
68
73
  const MIN_COL_W = 48;
69
74
  const storeKey = resizeStorageKey ? `table-cols:${resizeStorageKey}` : null;
@@ -277,28 +282,29 @@ export default function Table({ fields = [], headerGroups, items = [], renderRow
277
282
  const widthUnit = (i) => (resizeMode === 'fit' && !isFixedPx(i)) ? 'fr' : 'px';
278
283
  const canApplyWidths = colWidths && colWidths.length === fields.length
279
284
  && (resizeMode === 'fit' || items.length > 0);
280
- const effectiveGrid = canApplyWidths
285
+ const effectiveGrid = 좁은틀 ?? (canApplyWidths
281
286
  ? fields.map((f, i) => (f.resizable === false && defaultCols[i]) ? defaultCols[i] : `${colWidths[i]}${widthUnit(i)}`).join(' ')
282
- : gridTemplateColumns;
287
+ : gridTemplateColumns);
283
288
  const gridStyle = effectiveGrid
284
289
  ? { '--table-grid-cols': effectiveGrid }
285
290
  : undefined;
286
291
  const hasHeader = title !== undefined;
287
- return (_jsx("div", { className: `table-wrap table-wrap--${size}${resizable ? ' table-wrap--resizable' : ''}${dragOver ? ' table-wrap--dragover' : ''}${collapsed ? ' table-wrap--collapsed' : ''}`, style: gridStyle, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, children: _jsxs("div", { 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, onKeyDown: onToggleCollapse ? (e) => { if (e.key === 'Enter' || e.key === ' ') {
292
+ return (_jsx("div", { 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, children: _jsxs("div", { 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, onKeyDown: onToggleCollapse ? (e) => { if (e.key === 'Enter' || e.key === ' ') {
288
293
  e.preventDefault();
289
294
  onToggleCollapse();
290
- } } : undefined, children: [onToggleCollapse && (_jsx(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', resizable && '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,
295
+ } } : undefined, children: [onToggleCollapse && (_jsx(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,
291
296
  /* fit 모드는 오른쪽 이웃에서 폭을 덜어 오므로, 덜어 올 컬럼이 없는 자리에는
292
297
  손잡이를 두지 않는다 — 두면 그 컬럼만 grow처럼 넓어져 표가 밀린다. */
293
- onResizeStart: resizable && i < fields.length - 1 && f.resizable !== false
298
+ onResizeStart: 폭조절 && i < fields.length - 1 && f.resizable !== false
294
299
  && (resizeMode !== 'fit' || fields.some((g, j) => j > i && g.resizable !== false))
295
- ? (e) => startResize(i, e) : undefined, children: f.label }, i))) }), resizable && (_jsx(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) => {
300
+ ? (e) => startResize(i, e) : undefined, children: f.label }, i))) }), 폭조절 && (_jsx(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) => {
296
301
  const isDragging = draggingId != null && item.id === draggingId;
302
+ const 눌림 = !!onRowClick && (isRowClickable ? isRowClickable(item) : true);
297
303
  const isExpanded = expandedId != null && (expandedId instanceof Set ? expandedId.has(item.id) : item.id === expandedId);
298
- return (_jsx(TableRow, { className: `${onRowClick ? ' table-row--clickable' : ''}${onRowDragStart ? ' table-row--draggable' : ''}${isDragging ? ' table-row--dragging' : ''}`.trim(), draggable: !!onRowDragStart,
304
+ return (_jsx(TableRow, { className: `${눌림 ? ' table-row--clickable' : ''}${onRowDragStart ? ' table-row--draggable' : ''}${isDragging ? ' table-row--dragging' : ''}`.trim(), draggable: !!onRowDragStart,
299
305
  // 옆의 핸들러들처럼 있을 때만 넘긴다. 항상 함수를 만들어 넘기면
300
306
  // TableRow 가 "클릭 가능"으로 보고 role="button" · tabIndex=0 을 붙여,
301
307
  // 클릭 동작이 없는 표의 모든 행이 버튼으로 읽히고 탭 정지점이 된다.
302
- onClick: onRowClick ? () => 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))) }, String(item.id ?? idx)));
308
+ 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))) }, String(item.id ?? idx)));
303
309
  }) })] }))] }) }));
304
310
  }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cx } from '../utils/cx';
4
- import { useId, useEffect, useRef, useCallback } from 'react';
4
+ import { useId, useEffect, useLayoutEffect, useRef, useCallback, useState } from 'react';
5
5
  import { devWarnOnce } from '../utils/devWarn';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import Icon from './Icon';
@@ -53,7 +53,15 @@ export default function TextField(props) {
53
53
  const isControlled = value !== undefined;
54
54
  /* 비밀번호는 value 를 React 에 넘기지 않는다 — 위 useEffect 주석 참고 */
55
55
  const isPasswordControlled = isControlled && type === 'password';
56
- const len = (value ?? '').length;
56
+ /* 비제어 칸은 value 가 없으니 글자 수를 따로 센다 — 전에는 value 로만 세어 defaultValue 칸의 카운터는
57
+ 0, 지우기 단추는 안 보이고, 자동 높이는 늘지 않았다. */
58
+ const [innerLen, setInnerLen] = useState((defaultValue ?? '').length);
59
+ const len = isControlled ? (value ?? '').length : innerLen;
60
+ /* 비밀번호 controlled 칸이 부모가 거절·정규화한 값을 따라가게 — 아래 useEffect 는 value 가 바뀔 때만
61
+ 돌아, 부모가 같은 값을 다시 두면(공백을 걷는 등) 화면 칸에 친 글자가 남았다. React 가 일반 칸에
62
+ 해 주는 되돌림(restoreControlledState)을 onChange 뒤 한 박자에 흉내 낸다. */
63
+ const valueRef = useRef(value);
64
+ useLayoutEffect(() => { valueRef.current = value; });
57
65
  /*
58
66
  * React 는 controlled input 의 값을 node.defaultValue(= HTML value 속성)에 동기화한다.
59
67
  * 일반 입력에서는 무해하지만 비밀번호에서는 그 속성이 outerHTML 직렬화 경로
@@ -80,7 +88,7 @@ export default function TextField(props) {
80
88
  if (el.scrollHeight > el.clientHeight) {
81
89
  el.style.height = `${el.scrollHeight}px`;
82
90
  }
83
- }, [value, newline]);
91
+ }, [value, innerLen, newline]);
84
92
  const isOver = maxLength != null && len > maxLength;
85
93
  const iconSize = size === 32 ? 16 : 20;
86
94
  const cls = cx('text-field', `text-field--s${size}`, variant === 'display' ? 'text-field--display' : '', fullWidth ? 'text-field--full-width' : '', disabled ? 'text-field--disabled' : '', readOnly ? 'text-field--readonly' : '', error ? 'text-field--error' : '', leftIcon ? 'text-field--has-left-icon' : '', (rightIcon || clearable) ? 'text-field--has-right-icon' : '', className);
@@ -119,7 +127,21 @@ export default function TextField(props) {
119
127
  조용히 버린다. */
120
128
  'aria-controls': 목록떴나 ? combobox.listId : undefined,
121
129
  'aria-activedescendant': 목록떴나 && combobox.activeIdx >= 0 ? `${combobox.listId}-option-${combobox.activeIdx}` : undefined,
122
- onChange: (e) => onChange?.(e.target.value),
130
+ onChange: (e) => {
131
+ const node = e.target;
132
+ if (!isControlled)
133
+ setInnerLen(node.value.length);
134
+ onChange?.(node.value);
135
+ if (suggestions)
136
+ combobox.handleInput();
137
+ if (isPasswordControlled) {
138
+ queueMicrotask(() => {
139
+ const next = valueRef.current ?? '';
140
+ if (node.value !== next)
141
+ node.value = next;
142
+ });
143
+ }
144
+ },
123
145
  onFocus: ((e) => {
124
146
  onFocus?.(e);
125
147
  if (suggestions)
@@ -171,7 +193,14 @@ export default function TextField(props) {
171
193
  // 지우는 일은 click 에서 한다 — mousedown 에서만 처리하면 물리적 마우스 누름
172
194
  // 외에는 아무 반응이 없어, 이름표(aria-label)만 달린 못 누르는 버튼이 된다
173
195
  // (스크린리더의 활성화는 click 을 보낸다).
174
- onClick: () => onChange?.(''), tabIndex: -1 })), rightIcon && !showClear && (_jsx("span", { className: "text-field__icon text-field__icon--right", children: _jsx(Icon, { icon: rightIcon, label: "", className: "", svgClassName: "text-field__icon-svg", size: iconSize }) }))] }), suggestions && combobox.open && combobox.filtered.length > 0 && (_jsx("ul", { id: combobox.listId, className: "text-field__suggestions", role: "listbox", children: combobox.filtered.map((s, i) => (_jsx("li", { id: `${combobox.listId}-option-${i}`, role: "option", "aria-selected": i === combobox.activeIdx, className: `text-field__suggestion body-2${i === combobox.activeIdx ? ' is-active' : ''}`,
196
+ onClick: () => {
197
+ // 비제어 칸은 화면 값도 직접 비운다(onChange 만 부르면 글자가 그대로 남는다).
198
+ if (!isControlled && autoRef.current) {
199
+ autoRef.current.value = '';
200
+ setInnerLen(0);
201
+ }
202
+ onChange?.('');
203
+ }, tabIndex: -1 })), rightIcon && !showClear && (_jsx("span", { className: "text-field__icon text-field__icon--right", children: _jsx(Icon, { icon: rightIcon, label: "", className: "", svgClassName: "text-field__icon-svg", size: iconSize }) }))] }), suggestions && combobox.open && combobox.filtered.length > 0 && (_jsx("ul", { id: combobox.listId, className: "text-field__suggestions", role: "listbox", children: combobox.filtered.map((s, i) => (_jsx("li", { id: `${combobox.listId}-option-${i}`, role: "option", "aria-selected": i === combobox.activeIdx, className: `text-field__suggestion body-2${i === combobox.activeIdx ? ' is-active' : ''}`,
175
204
  // 선택은 click 에서 한다. onMouseDown 만 두면 마우스로는 눌리지만 스크린리더가
176
205
  // 이 option 을 활성화할 때는 click 만 발생해 아무 일도 일어나지 않는다.
177
206
  // mousedown 에서는 기본 동작(포커스 이동 → 입력 blur)을 막아, click 이 도달하기
@@ -85,6 +85,8 @@ export interface BarChartProps {
85
85
  loading?: boolean;
86
86
  /** 지정 시 헤더 우측에 '상세보기' 버튼 표시 → 클릭 콜백 */
87
87
  onDetail?: () => void;
88
+ /** CSS 격자 칸 안에 세운다 — 아래 여백·최소 폭을 풀고 막대를 카드 바닥에 맞춘다. ChartSection `inGrid` 참고. */
89
+ inGrid?: boolean;
88
90
  }
89
91
  /** 현재 컨트롤 선택(주/월)의 날짜 범위 — 상세보기(기간별 리스트)용 */
90
92
  export declare function getControlDateRange(control: BarChartControl): {
@@ -17,6 +17,7 @@ export declare function useComboboxSuggestions({ value, onChange, suggestions, o
17
17
  filtered: string[];
18
18
  handleFocus: () => void;
19
19
  handleBlur: () => void;
20
+ handleInput: () => void;
20
21
  handleKeyDown: (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
21
22
  handleSelect: (item: string) => void;
22
23
  };
@@ -25,11 +25,17 @@ export function useComboboxSuggestions({ value, onChange, suggestions, openOnFoc
25
25
  setActiveIdx(-1); }, [open]);
26
26
  useEffect(() => () => { if (blurTimerRef.current)
27
27
  clearTimeout(blurTimerRef.current); }, []);
28
+ /* 입력으로 후보가 줄면 가리키던 번호가 목록 밖에 남는다 — 그 채로 Enter 를 누르면 filtered[3] 같은
29
+ 없는 칸을 골라 onChange(undefined) 가 나갔다. 밖이면 '안 가리킴' 으로 본다. */
30
+ const safeIdx = activeIdx < filtered.length ? activeIdx : -1;
28
31
  const handleSelect = useCallback((item) => {
29
32
  onChange?.(item);
30
33
  setOpen(false);
31
34
  }, [onChange]);
32
35
  const handleFocus = useCallback(() => setOpen(true), []);
36
+ /* 글자를 치면 목록을 다시 연다 — 고르거나 Esc 로 닫은 뒤에는 포커스를 뺐다 넣어야만 열렸다.
37
+ 새 글자에는 새 후보가 서므로 가리킴도 처음으로 돌린다. */
38
+ const handleInput = useCallback(() => { setOpen(true); setActiveIdx(-1); }, []);
33
39
  // blur가 리스트 항목의 onMouseDown보다 먼저 발생하는 걸 막기 위한 딜레이
34
40
  const handleBlur = useCallback(() => {
35
41
  blurTimerRef.current = setTimeout(() => setOpen(false), 120);
@@ -38,16 +44,16 @@ export function useComboboxSuggestions({ value, onChange, suggestions, openOnFoc
38
44
  if (e.key === 'ArrowDown') {
39
45
  e.preventDefault();
40
46
  setOpen(true);
41
- setActiveIdx(i => Math.min(filtered.length - 1, i + 1));
47
+ setActiveIdx(i => Math.min(filtered.length - 1, (i < filtered.length ? i : -1) + 1));
42
48
  }
43
49
  else if (e.key === 'ArrowUp') {
44
50
  e.preventDefault();
45
- setActiveIdx(i => Math.max(-1, i - 1));
51
+ setActiveIdx(i => Math.max(-1, (i < filtered.length ? i : filtered.length) - 1));
46
52
  }
47
53
  else if (e.key === 'Enter') {
48
54
  // 한글 조합을 끝내는 Enter는 선택이 아니다 — 조합 중엔 제안을 고르지 않는다
49
- if (activeIdx >= 0 && !e.nativeEvent.isComposing)
50
- handleSelect(filtered[activeIdx]);
55
+ if (safeIdx >= 0 && !e.nativeEvent.isComposing)
56
+ handleSelect(filtered[safeIdx]);
51
57
  }
52
58
  else if (e.key === 'Escape') {
53
59
  if (open) {
@@ -55,6 +61,6 @@ export function useComboboxSuggestions({ value, onChange, suggestions, openOnFoc
55
61
  setOpen(false);
56
62
  } // 제안이 열렸을 때만 소비(상위 팝업 ESC보다 우선하지 않도록)
57
63
  }
58
- }, [filtered, activeIdx, open, handleSelect]);
59
- return { listId, open, activeIdx, filtered, handleFocus, handleBlur, handleKeyDown, handleSelect };
64
+ }, [filtered, safeIdx, open, handleSelect]);
65
+ return { listId, open, activeIdx: safeIdx, filtered, handleFocus, handleBlur, handleInput, handleKeyDown, handleSelect };
60
66
  }
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.131.0";
49
+ export declare const CORE_VERSION = "0.132.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.131.0';
49
+ export const CORE_VERSION = '0.132.0';