@ihabdevteam/core 0.206.0 → 0.207.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.206.0";
1629
+ export declare const CORE_VERSION = "0.207.0";
1630
1630
 
1631
1631
  /** @public */
1632
1632
  declare const coreDefault: Record<string, unknown>;
@@ -2379,7 +2379,12 @@ export declare interface FilterPanelProps {
2379
2379
  * CSS 로 오른쪽 끝까지 미는 억지를 썼다(말귀 단어표 만들기 2026-09-29).
2380
2380
  * **펼치면 그 자리를 `headerAction` 이 쓴다** — 둘은 같은 칸을 번갈아 쓴다.
2381
2381
  *
2382
- * 요약 **글월을 짓는 셈은 부르는 쪽** 에 있다. 코어는 자리만 낸다 —
2382
+ * **안 주면 코어가 짓는다** — `절 이름 + 고른 값` 을 ` · ` 으로 이어 한 줄로 만든다
2383
+ * (`등급 전체 · 음절 수 2음절`). 이름표도 값도 코어 것이라 부르는 쪽이 옮겨 적으면 두 벌이 되고,
2384
+ * 표 설정 판처럼 값이 `WordTableBlock` 안에 있는 자리는 부르는 쪽이 지을 길조차 없다
2385
+ * (말귀 요청 ㉗, 2026-09-29). 셈은 `utils/filterPanelSummary` 에 있다.
2386
+ *
2387
+ * **주면 준 것이 이긴다** — 글월을 부르는 쪽이 쥐고 싶을 때 쓴다. 코어는 자리만 낸다 —
2383
2388
  * 여러 조각을 넣을 때는 묶음에 `role="group"` 과 한 줄짜리 `aria-label` 을 주고 낱개는 `aria-hidden` 으로
2384
2389
  * 숨기는 편이 낫다(낭독기가 조각마다 끊어 읽지 않게). */
2385
2390
  headerSummary?: ReactNode;
@@ -13,6 +13,9 @@
13
13
  display: flex;
14
14
  align-items: center;
15
15
  justify-content: space-between;
16
+ /* 제목과 요약이 맞닿지 않게 — 요약이 길면 `space-between` 의 빈 곳이 0이 되어
17
+ '표 설정구성 이미지…' 로 붙어 읽힌다(실측 2026-09-29). */
18
+ gap: var(--spacing-s);
16
19
  }
17
20
 
18
21
  /* ── 접기 가능한 패널 ─────────────────────────────────────────
@@ -54,6 +57,10 @@
54
57
  .filter-panel__title {
55
58
  display: block;
56
59
  text-align: left;
60
+ /* 안 접힌다 — 넘치면 …로 자른다(title-row 의 `max-width` 에 닿았을 때) */
61
+ white-space: nowrap;
62
+ overflow: hidden;
63
+ text-overflow: ellipsis;
57
64
  }
58
65
 
59
66
  /* 헤더 좌측: 접기 버튼 + 타이틀 인라인 */
@@ -62,12 +69,29 @@
62
69
  align-items: center;
63
70
  gap: var(--spacing-xs);
64
71
  min-width: 0;
72
+ /* **제목이 먼저 자리를 얻고 요약이 양보한다.** 접힌 판의 요약은 길어서(표 설정은 절이 일곱이다)
73
+ 둘이 같이 줄면 짧은 제목이 두 줄로 접혀 60px 머리에 눌린다(실측 2026-09-29: '표 설정' 52px).
74
+ 그렇다고 제목이 머리를 다 먹으면 요약이 통째로 사라지므로 절반까지만 준다. */
75
+ flex-shrink: 0;
76
+ max-width: 50%;
77
+ }
78
+
79
+
80
+ /* 접힌 판이 제 속을 말하는 한 줄 — 길어지면 줄이지 말고 …로 자른다(머리는 한 줄 높이다).
81
+ `min-width: 0` 이 없으면 flex 칸이 제 속만큼 벌어져 제목을 밀어낸다. */
82
+ .filter-panel__header-summary {
83
+ color: var(--color-gray);
84
+ overflow: hidden;
85
+ text-overflow: ellipsis;
86
+ white-space: nowrap;
87
+ min-width: 0;
65
88
  }
66
89
 
67
90
  .filter-panel__header-actions {
68
91
  display: flex;
69
92
  align-items: center;
70
93
  gap: var(--spacing-xs);
94
+ min-width: 0;
71
95
  margin-left: auto;
72
96
  }
73
97
 
@@ -14,7 +14,12 @@ export interface FilterPanelProps {
14
14
  * CSS 로 오른쪽 끝까지 미는 억지를 썼다(말귀 단어표 만들기 2026-09-29).
15
15
  * **펼치면 그 자리를 `headerAction` 이 쓴다** — 둘은 같은 칸을 번갈아 쓴다.
16
16
  *
17
- * 요약 **글월을 짓는 셈은 부르는 쪽** 에 있다. 코어는 자리만 낸다 —
17
+ * **안 주면 코어가 짓는다** — `절 이름 + 고른 값` 을 ` · ` 으로 이어 한 줄로 만든다
18
+ * (`등급 전체 · 음절 수 2음절`). 이름표도 값도 코어 것이라 부르는 쪽이 옮겨 적으면 두 벌이 되고,
19
+ * 표 설정 판처럼 값이 `WordTableBlock` 안에 있는 자리는 부르는 쪽이 지을 길조차 없다
20
+ * (말귀 요청 ㉗, 2026-09-29). 셈은 `utils/filterPanelSummary` 에 있다.
21
+ *
22
+ * **주면 준 것이 이긴다** — 글월을 부르는 쪽이 쥐고 싶을 때 쓴다. 코어는 자리만 낸다 —
18
23
  * 여러 조각을 넣을 때는 묶음에 `role="group"` 과 한 줄짜리 `aria-label` 을 주고 낱개는 `aria-hidden` 으로
19
24
  * 숨기는 편이 낫다(낭독기가 조각마다 끊어 읽지 않게). */
20
25
  headerSummary?: ReactNode;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { cx } from '../utils/cx';
4
- import { Fragment, useState } from 'react';
4
+ import { Fragment, useMemo, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import './FilterPanel.css';
7
7
  import Button from './Button';
@@ -12,6 +12,7 @@ export { FILTER_ALL_VALUE } from '../types/filterPanel';
12
12
  import { FILTER_ALL_VALUE } from '../types/filterPanel';
13
13
  import FilterSectionInfo from './FilterSectionInfo';
14
14
  import { devWarnOnce } from '../utils/devWarn';
15
+ import { buildFilterSummary } from '../utils/filterPanelSummary';
15
16
  /** @public */
16
17
  export default function FilterPanel({ title, headerAction, headerSummary, collapsible = false, defaultCollapsed = false, collapsed: controlledCollapsed, onCollapsedChange, footer, sections, selected = {}, onSelect, align, sectionAlign, className, }) {
17
18
  // align 우선. 없으면 구 prop(sectionAlign) 매핑, 그것도 없으면 inline 기본.
@@ -39,13 +40,19 @@ export default function FilterPanel({ title, headerAction, headerSummary, collap
39
40
  return;
40
41
  toggleCollapsed();
41
42
  };
43
+ /* 안 주면 **코어가 짓는다** — 이름표도 값도 코어 것이라 부르는 쪽이 옮겨 적으면 두 벌이 되고,
44
+ 표 설정 판처럼 값이 아예 `WordTableBlock` 안에 있는 자리는 지을 길조차 없다(말귀 요청 ㉗).
45
+ `headerSummary` 를 주면 그것이 이긴다 — 글월을 부르는 쪽이 쥐던 길은 그대로 열려 있다.
46
+ 접혀 있을 때만 쓰이므로 펼친 동안은 셈하지 않는다. */
47
+ const 기본요약 = useMemo(() => (headerSummary !== undefined || !collapsed ? '' : buildFilterSummary(sections, selected, t('common.all', '전체'))), [headerSummary, collapsed, sections, selected, t]);
48
+ const summaryNode = headerSummary ?? (기본요약 ? _jsx("span", { className: "filter-panel__header-summary body-3", children: 기본요약 }) : null);
42
49
  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',
50
+ 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: [_jsx("div", { className: "filter-panel__title-row", children: title && _jsx("span", { className: "filter-panel__title h4", children: title }) }), _jsxs("div", { className: "filter-panel__header-actions", children: [collapsed ? summaryNode : headerAction, collapsible && (_jsx(Button, { htmlType: "button", size: 40, type: "dot", variant: "transparent-gray", useBorder: false, icon: collapsed ? 'chevron-down' : 'chevron-up',
44
51
  /* Button은 aria-label을 자기 ariaLabel prop으로 덮어쓴다({...rest} 뒤에
45
52
  aria-label={ariaLabelString}을 다시 쓴다) — kebab으로 넘기면 undefined가
46
53
  되어 이름이 통째로 사라진다. 저장소의 다른 호출부는 모두 ariaLabel을 쓴다.
47
54
  문구도 이 파일만 한국어로 박혀 있었다(키는 이미 있다). */
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 => {
55
+ className: "filter-panel__collapse-toggle", ariaLabel: collapsed ? t('common.expand', '펼치기') : t('common.collapse', '접기'), "aria-expanded": !collapsed, onClick: toggleCollapsed }))] })] })), !collapsed && (_jsxs(_Fragment, { children: [_jsx("div", { className: cx('filter-panel__sections', isRow ? 'filter-panel__sections--row' : ''), children: sections.map(section => {
49
56
  const sel = selected[section.label];
50
57
  const isAllSelected = sel === undefined || (Array.isArray(sel) ? sel.length === 0 : false);
51
58
  // section.items에 빈 항목(null/undefined)이 섞여 들어오면 item.value 접근에서
@@ -162,8 +162,13 @@ export default function WordSearchResultBlock({ titleIcon, resultCount, resultUn
162
162
  return (
163
163
  /* 제목·부제·조작부가 **상자 안** 에 선다(사용자 지시 2026-09-29) — 전에는 `Section` 의 머리가
164
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) => {
165
+ 이제 여백(--spacing-l)·부제(body-3 회색)를 그 판과 맞춘다.
166
+
167
+ **제목만 한 급 위(h3)다**(말귀 요청 ㉘, 2026-09-29). 0.206.0 에서는 곁의 `FilterPanel`
168
+ 과 아주 같은 h4 로 맞췄는데, 그러니 결과 덩이의 머리가 그 위 판 제목들(`검색 옵션` ·
169
+ `단어 검색 설정`)과 같은 급이 되어 **무엇이 무엇의 머리인지 안 보였다.** 거르개 판은
170
+ 결과를 고르는 도구고 이 덩이는 그 결과라, 나란한 것이 아니라 아래 딸린 것이다. */
171
+ _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 h3", 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) => {
167
172
  const isSelected = selected.includes(item.id);
168
173
  return (_jsx("div", { onMouseEnter: e => {
169
174
  openHover(e.currentTarget);
@@ -0,0 +1,14 @@
1
+ import type { FilterPanelSection } from '../types/filterPanel';
2
+ /**
3
+ * **접힌 거르개 판이 제 속을 한 줄로 말하는 글월**을 절과 고른 값에서 짓는다
4
+ * (`등급 전체 · 음절 수 2음절 · 품사 전체`).
5
+ *
6
+ * 왜 코어가 짓나: 이름표도 값도 코어 것이라 **부르는 쪽이 옮겨 적으면 두 벌이 된다.**
7
+ * 말귀가 등급·음절 수 이름표를 두 화면에 다르게 적어 한 번 어긋났고(말귀 요청 ㉗,
8
+ * 2026-09-29), 표 설정 판은 아예 값이 `WordTableBlock` 안에 있어 부르는 쪽이 글월을
9
+ * 지을 길조차 없었다. 그래서 **기본값은 코어가 짓고, 덮고 싶을 때만 `headerSummary`** 다.
10
+ *
11
+ * 안 담는 절: `note`(고르는 절이 아니다) · `input`(빈 칸을 '전체'라 말하면 거짓이다) ·
12
+ * 고른 것이 없는데 '전체' 보기도 없는 절(말할 것이 없다).
13
+ */
14
+ export declare function buildFilterSummary(sections: readonly FilterPanelSection[], selected: Record<string, string | string[]> | undefined, 전체말: string): string;
@@ -0,0 +1,57 @@
1
+ import { FILTER_ALL_VALUE } from '../types/filterPanel';
2
+ /**
3
+ * **접힌 거르개 판이 제 속을 한 줄로 말하는 글월**을 절과 고른 값에서 짓는다
4
+ * (`등급 전체 · 음절 수 2음절 · 품사 전체`).
5
+ *
6
+ * 왜 코어가 짓나: 이름표도 값도 코어 것이라 **부르는 쪽이 옮겨 적으면 두 벌이 된다.**
7
+ * 말귀가 등급·음절 수 이름표를 두 화면에 다르게 적어 한 번 어긋났고(말귀 요청 ㉗,
8
+ * 2026-09-29), 표 설정 판은 아예 값이 `WordTableBlock` 안에 있어 부르는 쪽이 글월을
9
+ * 지을 길조차 없었다. 그래서 **기본값은 코어가 짓고, 덮고 싶을 때만 `headerSummary`** 다.
10
+ *
11
+ * 안 담는 절: `note`(고르는 절이 아니다) · `input`(빈 칸을 '전체'라 말하면 거짓이다) ·
12
+ * 고른 것이 없는데 '전체' 보기도 없는 절(말할 것이 없다).
13
+ */
14
+ export function buildFilterSummary(sections, selected, 전체말) {
15
+ const 조각들 = [];
16
+ for (const section of sections ?? []) {
17
+ if (section.type === 'note' || section.type === 'input')
18
+ continue;
19
+ const 이름 = section.displayLabel ?? section.label;
20
+ const 값 = 값말(section, selected?.[section.label], 전체말);
21
+ if (값 === null)
22
+ continue;
23
+ 조각들.push(`${이름} ${값}`);
24
+ }
25
+ return 조각들.join(' · ');
26
+ }
27
+ /** 한 절이 무엇으로 정해져 있는지 — 말할 것이 없으면 `null` */
28
+ function 값말(section, sel, 전체말) {
29
+ const 고른것 = Array.isArray(sel) ? sel : sel === undefined ? [] : [sel];
30
+ /* '전체' 는 두 꼴로 온다 — 아무것도 안 고른 것과 '전체' 보기를 고른 것. 같은 말로 읽는다. */
31
+ const 실제 = 고른것.filter(v => v !== FILTER_ALL_VALUE);
32
+ if (실제.length === 0) {
33
+ return section.allOption ? (section.allLabel ?? 전체말) : null;
34
+ }
35
+ return 실제.map(v => 보기말(section, v)).join(', ');
36
+ }
37
+ /** 고른 값 하나를 사람이 읽는 말로 — 보기 목록에 없으면 값 그대로 */
38
+ function 보기말(section, value) {
39
+ const 보기들 = [
40
+ ...(section.items ?? []),
41
+ ...(section.groups ?? []).flat(),
42
+ ];
43
+ for (const item of 보기들) {
44
+ if (item == null)
45
+ continue;
46
+ if (typeof item === 'string') {
47
+ if (item === value)
48
+ return item;
49
+ continue;
50
+ }
51
+ if (item.value !== value)
52
+ continue;
53
+ /* 단추에 쓰는 차례 그대로 — `labels` 는 여러 줄로 쪼갠 것이라 한 줄로 이어 붙인다 */
54
+ return item.displayLabel ?? item.labels?.join(' ') ?? item.value;
55
+ }
56
+ return value;
57
+ }
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export declare const CORE_VERSION = "0.206.0";
49
+ export declare const CORE_VERSION = "0.207.0";
@@ -46,4 +46,4 @@
46
46
  *
47
47
  * 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
48
48
  */
49
- export const CORE_VERSION = '0.206.0';
49
+ export const CORE_VERSION = '0.207.0';