@ihabdevteam/core 0.205.0 → 0.206.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.205.0";
1629
+ export declare const CORE_VERSION = "0.206.0";
1630
1630
 
1631
1631
  /** @public */
1632
1632
  declare const coreDefault: Record<string, unknown>;
@@ -2350,7 +2350,7 @@ export declare const FILTER_ALL_VALUE = "\uC804\uCCB4";
2350
2350
  export declare function filterContrastCandidates(answer: string, candidates: Iterable<string>, opts: ContrastFilterOpts): string[];
2351
2351
 
2352
2352
  /** @public */
2353
- export declare function FilterPanel({ title, headerAction, collapsible, defaultCollapsed, collapsed: controlledCollapsed, onCollapsedChange, footer, sections, selected, onSelect, align, sectionAlign, className, }: FilterPanelProps): JSX.Element;
2353
+ export declare function FilterPanel({ title, headerAction, headerSummary, collapsible, defaultCollapsed, collapsed: controlledCollapsed, onCollapsedChange, footer, sections, selected, onSelect, align, sectionAlign, className, }: FilterPanelProps): JSX.Element;
2354
2354
 
2355
2355
  /** @public */
2356
2356
  export declare interface FilterPanelItem {
@@ -2372,7 +2372,20 @@ export declare interface FilterPanelItem {
2372
2372
  export declare interface FilterPanelProps {
2373
2373
  title?: ReactNode;
2374
2374
  headerAction?: ReactNode;
2375
- /** 헤더 우측에 접기/펼치기 토글 버튼을 노출하고, 접으면 본문(섹션)을 숨긴다. */
2375
+ /** 머리 오른쪽에 **접혀도 남는** 자리 — 접힌 판이 제 속을 한 줄로 말하게 한다("등급 전체 · 품사 전체" 같은 요약).
2376
+ *
2377
+ * `headerAction` 은 펼쳤을 때만 서고 접으면 사라진다(본문을 다루는 단추라 접힌 판에서는 할 일이 없다).
2378
+ * 그래서 접힌 판이 무엇을 담고 있는지 말할 자리가 `title` 밖에 없었고, 소비자가 제목 안에 요약을 싣고
2379
+ * CSS 로 오른쪽 끝까지 미는 억지를 썼다(말귀 단어표 만들기 2026-09-29).
2380
+ * **펼치면 그 자리를 `headerAction` 이 쓴다** — 둘은 같은 칸을 번갈아 쓴다.
2381
+ *
2382
+ * 요약 **글월을 짓는 셈은 부르는 쪽** 에 있다. 코어는 자리만 낸다 —
2383
+ * 여러 조각을 넣을 때는 묶음에 `role="group"` 과 한 줄짜리 `aria-label` 을 주고 낱개는 `aria-hidden` 으로
2384
+ * 숨기는 편이 낫다(낭독기가 조각마다 끊어 읽지 않게). */
2385
+ headerSummary?: ReactNode;
2386
+ /** 헤더 우측에 접기/펼치기 토글 버튼을 노출하고, 접으면 본문(섹션)을 숨긴다.
2387
+ * **머리 줄 전체가 손잡이다** — 머리 아무 데나 눌러도 접힌다(사용자 지시 2026-09-29).
2388
+ * `headerAction`·`headerSummary` 안의 단추·입력칸을 누른 것은 접지 않는다. */
2376
2389
  collapsible?: boolean;
2377
2390
  /** collapsible일 때 초기 접힘 상태 */
2378
2391
  defaultCollapsed?: boolean;
@@ -7265,6 +7278,16 @@ export declare function TableFilterPanel(props: TableFilterPanelProps): JSX.Elem
7265
7278
  declare interface TableFilterPanelBaseProps {
7266
7279
  /** 패널 제목. 미지정 시 i18n(wordTable.filterTitle) 기본값 사용. */
7267
7280
  title?: ReactNode;
7281
+ /** 머리에 접기 손잡이를 두고 머리 줄 전체를 손잡이로 만든다(`FilterPanel` 의 것). */
7282
+ collapsible?: boolean;
7283
+ /** 처음에 접힌 채로 연다. */
7284
+ defaultCollapsed?: boolean;
7285
+ /** 접힘을 바깥에서 쥔다(controlled). */
7286
+ collapsed?: boolean;
7287
+ /** 접힘이 바뀔 때 알린다. */
7288
+ onCollapsedChange?: (collapsed: boolean) => void;
7289
+ /** 접혀도 남는 머리 오른쪽 자리 — 요약. */
7290
+ headerSummary?: ReactNode;
7268
7291
  className?: string;
7269
7292
  }
7270
7293
 
@@ -10223,7 +10246,7 @@ export declare interface WordRow {
10223
10246
  * [단어 선정] 패널이 원 출처(섹션 구성은 다르지만 같은 셸을 공용).
10224
10247
  * @public
10225
10248
  */
10226
- export declare function WordSearchFilterPanel({ title, sections, selected, onSelect, className }: WordSearchFilterPanelProps): JSX.Element;
10249
+ export declare function WordSearchFilterPanel({ title, sections, selected, onSelect, collapsible, defaultCollapsed, collapsed, onCollapsedChange, headerSummary, headerAction, className, }: WordSearchFilterPanelProps): JSX.Element;
10227
10250
 
10228
10251
  /** @public */
10229
10252
  export declare interface WordSearchFilterPanelProps {
@@ -10237,6 +10260,18 @@ export declare interface WordSearchFilterPanelProps {
10237
10260
  sections: FilterPanelSection[];
10238
10261
  selected: Record<string, string | string[]>;
10239
10262
  onSelect: (value: string, sectionLabel: string) => void;
10263
+ /** 머리에 접기 손잡이를 두고 머리 줄 전체를 손잡이로 만든다 — `FilterPanel` 의 것을 그대로 넘긴다. */
10264
+ collapsible?: boolean;
10265
+ /** 처음에 접힌 채로 연다(`collapsible` 과 함께). */
10266
+ defaultCollapsed?: boolean;
10267
+ /** 접힘을 바깥에서 쥔다(controlled). */
10268
+ collapsed?: boolean;
10269
+ /** 접힘이 바뀔 때 알린다. */
10270
+ onCollapsedChange?: (collapsed: boolean) => void;
10271
+ /** 접혀도 남는 머리 오른쪽 자리 — 접힌 판이 제 속을 말하는 요약(`FilterPanel headerSummary`). */
10272
+ headerSummary?: ReactNode;
10273
+ /** 펼쳤을 때 머리 오른쪽에 서는 조작부(`FilterPanel headerAction`). */
10274
+ headerAction?: ReactNode;
10240
10275
  className?: string;
10241
10276
  }
10242
10277
 
@@ -10326,12 +10361,24 @@ export declare interface WordSearchResultWord {
10326
10361
  */
10327
10362
  export declare function WordTable({ title, titleIcon, disc, className, orientation, contentTypes, cutLine, showOutline, showDivLine, items, editCells, count, activeCell, onActivateCell, onRemoveCell, onRemoveItem, onReorder, renderCellAction, showFillerAdd, contentRef, buttonSize, pageNumber, pageNumberPosition, fitHeight, scoring, marks, onMarkClick, scoreIndexOffset, }: WordTableProps): JSX.Element;
10328
10363
 
10329
- export declare function WordTableBlock({ title, titleIcon, disc, items, onSaveComplete, onLoad, onRemoveItem, onReset, resetLabel, saveButtonLabel, secondarySaveButtonLabel, onReorder, showCellCount, onAddItem, className, renderSavePopup, renderWordSearch, hideControls, saveOnly, tableName, nameValue, onNameChange, descriptionValue, onDescriptionChange, namePlaceholder, descriptionPlaceholder, existingNames, editingName, onCancel, initialItemsPerPage, renderCellAction, actionsLeftExtra }: WordTableBlockProps): JSX.Element;
10364
+ export declare function WordTableBlock({ title, titleIcon, disc, filterTitle, filterCollapsible, filterDefaultCollapsed, filterHeaderSummary, items, onSaveComplete, onLoad, onRemoveItem, onReset, resetLabel, saveButtonLabel, secondarySaveButtonLabel, onReorder, showCellCount, onAddItem, className, renderSavePopup, renderWordSearch, hideControls, saveOnly, tableName, nameValue, onNameChange, descriptionValue, onDescriptionChange, namePlaceholder, descriptionPlaceholder, existingNames, editingName, onCancel, initialItemsPerPage, renderCellAction, actionsLeftExtra }: WordTableBlockProps): JSX.Element;
10330
10365
 
10331
10366
  /** @public */
10332
10367
  export declare interface WordTableBlockProps {
10333
10368
  title?: ReactNode;
10334
10369
  titleIcon?: string;
10370
+ /** 안쪽 **표 설정 판의 제목** — 안 주면 코어 기본값(`wordTable.filterTitle`, '표 설정').
10371
+ *
10372
+ * 화면마다 달라야 한다(단어표는 '단어표 설정', 대립쌍표는 그 화면 말) — 그래서 코어 기본값을 바꾸는 것으로는 안 되고,
10373
+ * 앱 사전으로 코어 열쇠를 덮는 것은 말귀의 `guards/i18nOwnership` 이 막는다(옳은 규칙이다).
10374
+ * **부르는 쪽이 제 열쇠로 지어 넘긴다**(말귀 2026-09-29). */
10375
+ filterTitle?: ReactNode;
10376
+ /** 안쪽 표 설정 판을 접을 수 있게 한다 — 머리 줄 전체가 손잡이다. */
10377
+ filterCollapsible?: boolean;
10378
+ /** 그 판을 처음에 접힌 채로 연다(`filterCollapsible` 과 함께). */
10379
+ filterDefaultCollapsed?: boolean;
10380
+ /** 그 판이 접혀도 머리 오른쪽에 남는 자리 — 요약. */
10381
+ filterHeaderSummary?: ReactNode;
10335
10382
  disc?: ReactNode;
10336
10383
  /** 현재 표의 저장된 이름(불러온 표 등). 있으면 인쇄/PDF 저장 시 파일명으로 사용, 없으면 "오늘 날짜 + 단어표/대립쌍표" */
10337
10384
  tableName?: string;
@@ -26,6 +26,18 @@
26
26
  .filter-panel--collapsible .filter-panel__header {
27
27
  min-height: 60px;
28
28
  flex-shrink: 0;
29
+ /* 머리 줄 전체가 손잡이다(사용자 지시 2026-09-29) — 소비자가 감싸개에 onClick 을 달아
30
+ 머리 안을 눌렀는지 보던 자리다. 안의 단추·입력칸을 누른 것은 컴포넌트가 걸러 낸다. */
31
+ cursor: pointer;
32
+ }
33
+ /* 접기 손잡이는 **아이콘만** 보인다 — 단추의 바탕·네모 티를 지운다(사용자 지시).
34
+ 포커스 테두리는 남긴다: 키보드 쓰는 사람에게 그 자리가 어디인지 알리는 유일한 표시다. */
35
+ .filter-panel__collapse-toggle,
36
+ .filter-panel__collapse-toggle:hover,
37
+ .filter-panel__collapse-toggle:active {
38
+ background: transparent;
39
+ border: none;
40
+ box-shadow: none;
29
41
  }
30
42
  .filter-panel--collapsible .filter-panel__sections {
31
43
  padding-bottom: var(--spacing-m);
@@ -7,7 +7,20 @@ import type { FilterPanelSection } from '../types/filterPanel';
7
7
  export interface FilterPanelProps {
8
8
  title?: ReactNode;
9
9
  headerAction?: ReactNode;
10
- /** 헤더 우측에 접기/펼치기 토글 버튼을 노출하고, 접으면 본문(섹션)을 숨긴다. */
10
+ /** 머리 오른쪽에 **접혀도 남는** 자리 — 접힌 판이 제 속을 한 줄로 말하게 한다("등급 전체 · 품사 전체" 같은 요약).
11
+ *
12
+ * `headerAction` 은 펼쳤을 때만 서고 접으면 사라진다(본문을 다루는 단추라 접힌 판에서는 할 일이 없다).
13
+ * 그래서 접힌 판이 무엇을 담고 있는지 말할 자리가 `title` 밖에 없었고, 소비자가 제목 안에 요약을 싣고
14
+ * CSS 로 오른쪽 끝까지 미는 억지를 썼다(말귀 단어표 만들기 2026-09-29).
15
+ * **펼치면 그 자리를 `headerAction` 이 쓴다** — 둘은 같은 칸을 번갈아 쓴다.
16
+ *
17
+ * 요약 **글월을 짓는 셈은 부르는 쪽** 에 있다. 코어는 자리만 낸다 —
18
+ * 여러 조각을 넣을 때는 묶음에 `role="group"` 과 한 줄짜리 `aria-label` 을 주고 낱개는 `aria-hidden` 으로
19
+ * 숨기는 편이 낫다(낭독기가 조각마다 끊어 읽지 않게). */
20
+ headerSummary?: ReactNode;
21
+ /** 헤더 우측에 접기/펼치기 토글 버튼을 노출하고, 접으면 본문(섹션)을 숨긴다.
22
+ * **머리 줄 전체가 손잡이다** — 머리 아무 데나 눌러도 접힌다(사용자 지시 2026-09-29).
23
+ * `headerAction`·`headerSummary` 안의 단추·입력칸을 누른 것은 접지 않는다. */
11
24
  collapsible?: boolean;
12
25
  /** collapsible일 때 초기 접힘 상태 */
13
26
  defaultCollapsed?: boolean;
@@ -31,4 +44,4 @@ export interface FilterPanelProps {
31
44
  className?: string;
32
45
  }
33
46
  /** @public */
34
- export default function FilterPanel({ title, headerAction, collapsible, defaultCollapsed, collapsed: controlledCollapsed, onCollapsedChange, footer, sections, selected, onSelect, align, sectionAlign, className, }: FilterPanelProps): import("react").JSX.Element;
47
+ export default function FilterPanel({ title, headerAction, headerSummary, collapsible, defaultCollapsed, collapsed: controlledCollapsed, onCollapsedChange, footer, sections, selected, onSelect, align, sectionAlign, className, }: FilterPanelProps): import("react").JSX.Element;
@@ -13,7 +13,7 @@ import { FILTER_ALL_VALUE } from '../types/filterPanel';
13
13
  import FilterSectionInfo from './FilterSectionInfo';
14
14
  import { devWarnOnce } from '../utils/devWarn';
15
15
  /** @public */
16
- export default function FilterPanel({ title, headerAction, collapsible = false, defaultCollapsed = false, collapsed: controlledCollapsed, onCollapsedChange, footer, sections, selected = {}, onSelect, align, sectionAlign, className, }) {
16
+ export default function FilterPanel({ title, headerAction, headerSummary, collapsible = false, defaultCollapsed = false, collapsed: controlledCollapsed, onCollapsedChange, footer, sections, selected = {}, onSelect, align, sectionAlign, className, }) {
17
17
  // align 우선. 없으면 구 prop(sectionAlign) 매핑, 그것도 없으면 inline 기본.
18
18
  const isRow = (align ?? (sectionAlign === 'vertical' ? 'newline' : sectionAlign === 'horizontal' ? 'inline' : 'inline')) === 'inline';
19
19
  const { t } = useTranslation();
@@ -24,13 +24,28 @@ export default function FilterPanel({ title, headerAction, collapsible = false,
24
24
  setInternalCollapsed(next);
25
25
  onCollapsedChange?.(next);
26
26
  };
27
- const showHeader = title || headerAction || collapsible;
28
- return (_jsxs("div", { className: cx('filter-panel', collapsible ? 'filter-panel--collapsible' : '', collapsed ? 'filter-panel--collapsed' : '', className), children: [showHeader && (_jsxs("div", { className: "filter-panel__header", children: [_jsxs("div", { className: "filter-panel__title-row", children: [collapsible && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-down' : 'chevron-up',
27
+ /* 머리 아무 데나 눌러도 접힌다. 다만 **그 안의 조작부를 누른 것은 아니다** —
28
+ `headerAction` 의 단추(초기화 등)를 누르면 그 일만 하고 판은 그대로 둔다.
29
+ chevron 단추는 제 `onClick` 으로 접으므로 여기서 한 번 더 접지 않는다(두 번 접히면 제자리다). */
30
+ const 머리누름 = (e) => {
31
+ if (!collapsible)
32
+ return;
33
+ const el = e.target;
34
+ if (!el || typeof el.closest !== 'function')
35
+ return;
36
+ if (el.closest('.filter-panel__collapse-toggle'))
37
+ return;
38
+ if (el.closest('button, a, input, select, textarea, label, [role="radiogroup"], [role="button"], [role="menu"]'))
39
+ return;
40
+ toggleCollapsed();
41
+ };
42
+ const showHeader = title || headerAction || headerSummary || collapsible;
43
+ return (_jsxs("div", { className: cx('filter-panel', collapsible ? 'filter-panel--collapsible' : '', collapsed ? 'filter-panel--collapsed' : '', className), children: [showHeader && (_jsxs("div", { className: "filter-panel__header", onClick: collapsible ? 머리누름 : undefined, children: [_jsxs("div", { className: "filter-panel__title-row", children: [collapsible && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-down' : 'chevron-up',
29
44
  /* Button은 aria-label을 자기 ariaLabel prop으로 덮어쓴다({...rest} 뒤에
30
45
  aria-label={ariaLabelString}을 다시 쓴다) — kebab으로 넘기면 undefined가
31
46
  되어 이름이 통째로 사라진다. 저장소의 다른 호출부는 모두 ariaLabel을 쓴다.
32
47
  문구도 이 파일만 한국어로 박혀 있었다(키는 이미 있다). */
33
- ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), "aria-expanded": !collapsed, onClick: toggleCollapsed })), title && _jsx("span", { className: "filter-panel__title h4", children: title })] }), _jsx("div", { className: "filter-panel__header-actions", children: !collapsed && headerAction })] })), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { className: cx('filter-panel__sections', isRow ? 'filter-panel__sections--row' : ''), children: sections.map(section => {
48
+ className: "filter-panel__collapse-toggle", ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), "aria-expanded": !collapsed, onClick: toggleCollapsed })), title && _jsx("span", { className: "filter-panel__title h4", children: title })] }), _jsx("div", { className: "filter-panel__header-actions", children: collapsed ? headerSummary : headerAction })] })), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { className: cx('filter-panel__sections', isRow ? 'filter-panel__sections--row' : ''), children: sections.map(section => {
34
49
  const sel = selected[section.label];
35
50
  const isAllSelected = sel === undefined || (Array.isArray(sel) ? sel.length === 0 : false);
36
51
  // section.items에 빈 항목(null/undefined)이 섞여 들어오면 item.value 접근에서
@@ -5,6 +5,16 @@ import { type TableSettingsSetters } from './wordTableSettings';
5
5
  interface TableFilterPanelBaseProps {
6
6
  /** 패널 제목. 미지정 시 i18n(wordTable.filterTitle) 기본값 사용. */
7
7
  title?: ReactNode;
8
+ /** 머리에 접기 손잡이를 두고 머리 줄 전체를 손잡이로 만든다(`FilterPanel` 의 것). */
9
+ collapsible?: boolean;
10
+ /** 처음에 접힌 채로 연다. */
11
+ defaultCollapsed?: boolean;
12
+ /** 접힘을 바깥에서 쥔다(controlled). */
13
+ collapsed?: boolean;
14
+ /** 접힘이 바뀔 때 알린다. */
15
+ onCollapsedChange?: (collapsed: boolean) => void;
16
+ /** 접혀도 남는 머리 오른쪽 자리 — 요약. */
17
+ headerSummary?: ReactNode;
8
18
  className?: string;
9
19
  }
10
20
  /**
@@ -16,9 +16,11 @@ function isShellProps(props) {
16
16
  */
17
17
  export default function TableFilterPanel(props) {
18
18
  const { t } = useTranslation();
19
- const { title, className } = props;
19
+ const { title, className, collapsible, defaultCollapsed, collapsed, onCollapsedChange, headerSummary } = props;
20
+ /* 두 갈래(껍데기 모드·표 설정 모드)가 같은 접기 값을 쓴다 — 한쪽만 넘기면 화면마다 다르게 접힌다 */
21
+ const 접기 = { collapsible, defaultCollapsed, collapsed, onCollapsedChange, headerSummary };
20
22
  if (isShellProps(props)) {
21
- return (_jsx(FilterPanel, { className: className, title: title ?? t('wordTable.filterTitle', '표 설정'), sections: props.sections, selected: props.selected, onSelect: props.onSelect, align: "inline" }));
23
+ return (_jsx(FilterPanel, { className: className, title: title ?? t('wordTable.filterTitle', '표 설정'), sections: props.sections, selected: props.selected, onSelect: props.onSelect, ...접기, align: "inline" }));
22
24
  }
23
25
  const { hasImage, hasColloquial, contentTypes, orientation, showOutline, cutLine, showDivLine, setters, showCutLine = false, showDivLine2 = false, middleSections = [], extraSelected = {}, onExtraSelect, } = props;
24
26
  const sections = [
@@ -38,5 +40,5 @@ export default function TableFilterPanel(props) {
38
40
  return;
39
41
  onExtraSelect?.(value, sectionLabel);
40
42
  };
41
- return (_jsx(FilterPanel, { className: className, title: title ?? t('wordTable.filterTitle', '표 설정'), sections: sections, selected: selected, onSelect: onSelect, sectionAlign: "horizontal" }));
43
+ return (_jsx(FilterPanel, { className: className, title: title ?? t('wordTable.filterTitle', '표 설정'), sections: sections, selected: selected, onSelect: onSelect, ...접기, sectionAlign: "horizontal" }));
42
44
  }
@@ -12,6 +12,18 @@ export interface WordSearchFilterPanelProps {
12
12
  sections: FilterPanelSection[];
13
13
  selected: Record<string, string | string[]>;
14
14
  onSelect: (value: string, sectionLabel: string) => void;
15
+ /** 머리에 접기 손잡이를 두고 머리 줄 전체를 손잡이로 만든다 — `FilterPanel` 의 것을 그대로 넘긴다. */
16
+ collapsible?: boolean;
17
+ /** 처음에 접힌 채로 연다(`collapsible` 과 함께). */
18
+ defaultCollapsed?: boolean;
19
+ /** 접힘을 바깥에서 쥔다(controlled). */
20
+ collapsed?: boolean;
21
+ /** 접힘이 바뀔 때 알린다. */
22
+ onCollapsedChange?: (collapsed: boolean) => void;
23
+ /** 접혀도 남는 머리 오른쪽 자리 — 접힌 판이 제 속을 말하는 요약(`FilterPanel headerSummary`). */
24
+ headerSummary?: ReactNode;
25
+ /** 펼쳤을 때 머리 오른쪽에 서는 조작부(`FilterPanel headerAction`). */
26
+ headerAction?: ReactNode;
15
27
  className?: string;
16
28
  }
17
29
  /**
@@ -20,4 +32,4 @@ export interface WordSearchFilterPanelProps {
20
32
  * [단어 선정] 패널이 원 출처(섹션 구성은 다르지만 같은 셸을 공용).
21
33
  * @public
22
34
  */
23
- export default function WordSearchFilterPanel({ title, sections, selected, onSelect, className }: WordSearchFilterPanelProps): import("react").JSX.Element;
35
+ export default function WordSearchFilterPanel({ title, sections, selected, onSelect, collapsible, defaultCollapsed, collapsed, onCollapsedChange, headerSummary, headerAction, className, }: WordSearchFilterPanelProps): import("react").JSX.Element;
@@ -2,13 +2,15 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import FilterPanel from '../FilterPanel';
5
+ /* 이 넷이 없어서 소비자가 **껍데기를 건너뛰고 `FilterPanel` 을 직접** 쓰고 있었다 —
6
+ 그러면 이 껍데기가 쥔 두 줄(i18n 제목·`align="inline"`)이 소비자 쪽에 복사된다(말귀 2026-09-29). */
5
7
  /**
6
8
  * 단어를 어떤 조건으로 좁힐지 고르는 FilterPanel 셸(shell) — 등급/품사/어종/음절 수/선택 섹션 자체는
7
9
  * 호출부가 구성해 넘긴다. WordSearchPage의 [단어 검색 설정] 패널과 WordTrainingFilterPanels의
8
10
  * [단어 선정] 패널이 원 출처(섹션 구성은 다르지만 같은 셸을 공용).
9
11
  * @public
10
12
  */
11
- export default function WordSearchFilterPanel({ title, sections, selected, onSelect, className }) {
13
+ export default function WordSearchFilterPanel({ title, sections, selected, onSelect, collapsible, defaultCollapsed, collapsed, onCollapsedChange, headerSummary, headerAction, className, }) {
12
14
  const { t } = useTranslation();
13
- return (_jsx(FilterPanel, { className: className, title: title ?? t('wordSearch.filterTitle', '단어 검색 설정'), sections: sections, selected: selected, onSelect: onSelect, align: "inline" }));
15
+ return (_jsx(FilterPanel, { className: className, title: title ?? t('wordSearch.filterTitle', '단어 검색 설정'), sections: sections, selected: selected, onSelect: onSelect, collapsible: collapsible, defaultCollapsed: defaultCollapsed, collapsed: collapsed, onCollapsedChange: onCollapsedChange, headerSummary: headerSummary, headerAction: headerAction, align: "inline" }));
14
16
  }
@@ -20,7 +20,46 @@
20
20
  border: 1px solid var(--color-darkWhite);
21
21
  background: var(--color-white);
22
22
  border-radius: var(--radius-m);
23
- padding: var(--spacing-content);
23
+ /* 옆에 서는 `FilterPanel` 과 같은 여백(사용자 지시 2026-09-29) */
24
+ padding: var(--spacing-l);
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--spacing-m);
28
+ }
29
+
30
+ /* 상자 안 머리 — `FilterPanel` 의 머리와 같은 결(제목 h4 · 부제 body-3 회색).
31
+ 설명은 한 줄을 통째로 쓴다(제목 옆에 두면 조작부를 다음 줄로 밀어낸다). */
32
+ .word-search-result-block__head {
33
+ display: flex;
34
+ align-items: center;
35
+ flex-wrap: wrap;
36
+ gap: var(--spacing-xs);
37
+ }
38
+ .word-search-result-block__head-main {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: var(--spacing-xs);
42
+ min-width: 0;
43
+ }
44
+ .word-search-result-block__head-icon {
45
+ flex-shrink: 0;
46
+ color: var(--color-gray);
47
+ }
48
+ .word-search-result-block__title {
49
+ text-align: left;
50
+ min-width: 0;
51
+ }
52
+ .word-search-result-block__head-actions {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: var(--spacing-xs);
56
+ margin-left: auto;
57
+ }
58
+ .word-search-result-block__head-disc {
59
+ flex: 0 0 100%;
60
+ min-width: 0;
61
+ color: var(--color-gray);
62
+ margin: 0;
24
63
  }
25
64
 
26
65
  /* 박스↔액션 간격은 '내용이 있는' 액션(더보기/선택초기화/인쇄)이 뒤에 올 때만 준다.
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
7
7
  import './WordSearchResultBlock.css';
8
8
  import Button from '../Button';
9
9
  import Section from '../Section';
10
+ import Icon from '../Icon';
10
11
  import Dropdown from '../Dropdown';
11
12
  import Pagination from '../Pagination';
12
13
  import WordImageTooltip from '../Tooltip/WordImageTooltip';
@@ -158,7 +159,11 @@ export default function WordSearchResultBlock({ titleIcon, resultCount, resultUn
158
159
  const discNode = disc ?? (notice
159
160
  ? _jsxs(_Fragment, { children: [_jsx("span", { className: "word-search-result-block__notice", children: notice }), discLabel ? _jsx("span", { className: "word-search-result-block__disc", children: discLabel }) : null] })
160
161
  : discLabel);
161
- return (_jsxs(Section, { type: sectionType, icon: titleIcon, title: titleLabel, action: toolbar, disc: discNode, className: cx('word-search-result-block', className), children: [_jsxs("div", { className: "word-search-result-block__inner", children: [(groups ? groups.length === 0 : items.length === 0) && (_jsxs("div", { className: "word-search-result-block__empty-wrap", children: [_jsx("p", { className: "word-search-result-block__empty body-3", children: t('wordSearch.emptyResult', '검색 결과가 없습니다.') }), emptyHint && _jsx("p", { className: "word-search-result-block__empty-hint body-3", children: emptyHint })] })), groups ? ((pagedGroups ?? []).map((group) => (_jsx("div", { className: "word-search-result-block__group", children: _jsx("div", { className: "word-search-result-block__items", children: group.items.map((item) => {
162
+ return (
163
+ /* 제목·부제·조작부가 **상자 안** 에 선다(사용자 지시 2026-09-29) — 전에는 `Section` 의 머리가
164
+ 상자 밖에 서서 제목이 테두리 위에 뜨고 좌우 여백도 옆의 `FilterPanel` 과 달라 보였다.
165
+ 이제 여백(--spacing-l)·제목 등급(h4)·부제(body-3 회색)를 그 판과 맞춘다. */
166
+ _jsxs(Section, { type: sectionType, className: cx('word-search-result-block', className), children: [_jsxs("div", { className: "word-search-result-block__inner", children: [(titleLabel || toolbar || discNode) && (_jsxs("div", { className: "word-search-result-block__head", children: [_jsxs("div", { className: "word-search-result-block__head-main", children: [titleIcon && _jsx(Icon, { icon: titleIcon, className: "word-search-result-block__head-icon" }), titleLabel && _jsx("span", { className: "word-search-result-block__title h4", children: titleLabel })] }), toolbar && _jsx("div", { className: "word-search-result-block__head-actions", children: toolbar }), discNode && _jsx("div", { className: "word-search-result-block__head-disc body-3", children: discNode })] })), (groups ? groups.length === 0 : items.length === 0) && (_jsxs("div", { className: "word-search-result-block__empty-wrap", children: [_jsx("p", { className: "word-search-result-block__empty body-3", children: t('wordSearch.emptyResult', '검색 결과가 없습니다.') }), emptyHint && _jsx("p", { className: "word-search-result-block__empty-hint body-3", children: emptyHint })] })), groups ? ((pagedGroups ?? []).map((group) => (_jsx("div", { className: "word-search-result-block__group", children: _jsx("div", { className: "word-search-result-block__items", children: group.items.map((item) => {
162
167
  const isSelected = selected.includes(item.id);
163
168
  return (_jsx("div", { onMouseEnter: e => {
164
169
  openHover(e.currentTarget);
@@ -1,4 +1,4 @@
1
1
  import './WordTableBlock.css';
2
2
  import type { WordTableBlockProps } from './wordTableBlockTypes';
3
3
  export type { WordEntry, WordTableBlockProps } from './wordTableBlockTypes';
4
- export default function WordTableBlock({ title, titleIcon, disc, items, onSaveComplete, onLoad, onRemoveItem, onReset, resetLabel, saveButtonLabel, secondarySaveButtonLabel, onReorder, showCellCount, onAddItem, className, renderSavePopup, renderWordSearch, hideControls, saveOnly, tableName, nameValue, onNameChange, descriptionValue, onDescriptionChange, namePlaceholder, descriptionPlaceholder, existingNames, editingName, onCancel, initialItemsPerPage, renderCellAction, actionsLeftExtra }: WordTableBlockProps): import("react").JSX.Element;
4
+ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, filterCollapsible, filterDefaultCollapsed, filterHeaderSummary, items, onSaveComplete, onLoad, onRemoveItem, onReset, resetLabel, saveButtonLabel, secondarySaveButtonLabel, onReorder, showCellCount, onAddItem, className, renderSavePopup, renderWordSearch, hideControls, saveOnly, tableName, nameValue, onNameChange, descriptionValue, onDescriptionChange, namePlaceholder, descriptionPlaceholder, existingNames, editingName, onCancel, initialItemsPerPage, renderCellAction, actionsLeftExtra }: WordTableBlockProps): import("react").JSX.Element;
@@ -45,7 +45,7 @@ function 겹치지않는id(list) {
45
45
  return id === it.id ? it : { ...it, id };
46
46
  });
47
47
  }
48
- export default function WordTableBlock({ title, titleIcon, disc, items, onSaveComplete, onLoad, onRemoveItem, onReset, resetLabel, saveButtonLabel, secondarySaveButtonLabel, onReorder, showCellCount, onAddItem, className, renderSavePopup, renderWordSearch, hideControls, saveOnly, tableName, nameValue, onNameChange, descriptionValue, onDescriptionChange, namePlaceholder, descriptionPlaceholder, existingNames, editingName, onCancel, initialItemsPerPage, renderCellAction, actionsLeftExtra }) {
48
+ export default function WordTableBlock({ title, titleIcon, disc, filterTitle, filterCollapsible, filterDefaultCollapsed, filterHeaderSummary, items, onSaveComplete, onLoad, onRemoveItem, onReset, resetLabel, saveButtonLabel, secondarySaveButtonLabel, onReorder, showCellCount, onAddItem, className, renderSavePopup, renderWordSearch, hideControls, saveOnly, tableName, nameValue, onNameChange, descriptionValue, onDescriptionChange, namePlaceholder, descriptionPlaceholder, existingNames, editingName, onCancel, initialItemsPerPage, renderCellAction, actionsLeftExtra }) {
49
49
  const { t } = useTranslation();
50
50
  const resolvedResetLabel = resetLabel ?? t('wordPrint.clearAllSelection', '모두 선택 해제');
51
51
  const isEditable = items === undefined;
@@ -318,7 +318,7 @@ export default function WordTableBlock({ title, titleIcon, disc, items, onSaveCo
318
318
  setActiveCell(null);
319
319
  },
320
320
  onClose: handleCloseWordSearch,
321
- }), !hideControls && (_jsx(TableFilterPanel, { hasImage: hasImage, hasColloquial: hasColloquial, contentTypes: contentTypes, orientation: orientation, showOutline: showOutline, cutLine: cutLine, showDivLine: showDivLine, setters: { setContentTypes, setOrientation, setShowOutline, setCutLine, setShowDivLine }, showCutLine: (pagesEnabled ? itemsPerPage : count) > 1, showDivLine2: hasPairs, middleSections: middleSections, extraSelected: middleSelected, onExtraSelect: handleMiddleSelect })), isPaginated && pages ? (_jsx("div", { className: "word-table-block__pages", children: pages.map((page, i) => (_jsx(WordTable, { orientation: orientation, contentTypes: contentTypes, cutLine: cutLine, showOutline: showOutline, showDivLine: showDivLine, count: itemsPerPage, items: page, onRemoveItem: onRemoveItem,
321
+ }), !hideControls && (_jsx(TableFilterPanel, { title: filterTitle, collapsible: filterCollapsible, defaultCollapsed: filterDefaultCollapsed, headerSummary: filterHeaderSummary, hasImage: hasImage, hasColloquial: hasColloquial, contentTypes: contentTypes, orientation: orientation, showOutline: showOutline, cutLine: cutLine, showDivLine: showDivLine, setters: { setContentTypes, setOrientation, setShowOutline, setCutLine, setShowDivLine }, showCutLine: (pagesEnabled ? itemsPerPage : count) > 1, showDivLine2: hasPairs, middleSections: middleSections, extraSelected: middleSelected, onExtraSelect: handleMiddleSelect })), isPaginated && pages ? (_jsx("div", { className: "word-table-block__pages", children: pages.map((page, i) => (_jsx(WordTable, { orientation: orientation, contentTypes: contentTypes, cutLine: cutLine, showOutline: showOutline, showDivLine: showDivLine, count: itemsPerPage, items: page, onRemoveItem: onRemoveItem,
322
322
  /* 쪽이 나뉘면 끌어 옮기기가 통째로 꺼졌다(말귀 단어 찾기: 9개부터). 쪽 안의 자리를 전체 자리로 옮겨 넘긴다 —
323
323
  쪽을 넘는 이동은 하지 않는다. */
324
324
  onReorder: onReorder && ((from, to) => onReorder(from + i * itemsPerPage, to + i * itemsPerPage)),
@@ -18,6 +18,18 @@ export interface WordEntry {
18
18
  export interface WordTableBlockProps {
19
19
  title?: ReactNode;
20
20
  titleIcon?: string;
21
+ /** 안쪽 **표 설정 판의 제목** — 안 주면 코어 기본값(`wordTable.filterTitle`, '표 설정').
22
+ *
23
+ * 화면마다 달라야 한다(단어표는 '단어표 설정', 대립쌍표는 그 화면 말) — 그래서 코어 기본값을 바꾸는 것으로는 안 되고,
24
+ * 앱 사전으로 코어 열쇠를 덮는 것은 말귀의 `guards/i18nOwnership` 이 막는다(옳은 규칙이다).
25
+ * **부르는 쪽이 제 열쇠로 지어 넘긴다**(말귀 2026-09-29). */
26
+ filterTitle?: ReactNode;
27
+ /** 안쪽 표 설정 판을 접을 수 있게 한다 — 머리 줄 전체가 손잡이다. */
28
+ filterCollapsible?: boolean;
29
+ /** 그 판을 처음에 접힌 채로 연다(`filterCollapsible` 과 함께). */
30
+ filterDefaultCollapsed?: boolean;
31
+ /** 그 판이 접혀도 머리 오른쪽에 남는 자리 — 요약. */
32
+ filterHeaderSummary?: ReactNode;
21
33
  disc?: ReactNode;
22
34
  /** 현재 표의 저장된 이름(불러온 표 등). 있으면 인쇄/PDF 저장 시 파일명으로 사용, 없으면 "오늘 날짜 + 단어표/대립쌍표" */
23
35
  tableName?: string;
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.205.0";
49
+ export declare const CORE_VERSION = "0.206.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.205.0';
49
+ export const CORE_VERSION = '0.206.0';