@iyulab/chat-components 0.17.4 → 0.18.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.18.0] - 2026-10-07
4
+
5
+ ### Changed (breaking)
6
+
7
+ - **`u-table-block` cell `text` is plain text.** It was rendered as HTML, so a string meant as text — a name, a
8
+ server value — injected markup, and search, sort and the CSV/XLS downloads read the tags too (a bold `9` sorted
9
+ after `10`; searching `strong` matched every bold cell; downloads carried `<strong>`). A formatted cell now gives
10
+ its markup in the new optional `html` field, used only for display. `u-marked-block` fills both, so markdown
11
+ tables look the same. **Migration:** if you passed markup in `text`, move it to `html` and put the plain text in
12
+ `text`.
13
+
14
+ ### Fixed
15
+
16
+ - **Search highlighting no longer cuts markup.** It inserted `<mark>` into the cell's HTML string, splitting tags and
17
+ entities (searching `amp` broke `&amp;`). Matches are now painted with the CSS Custom Highlight API over the
18
+ rendered text; the DOM is unchanged.
19
+
20
+ ## [0.17.5] - 2026-10-07
21
+
22
+ ### Fixed
23
+
24
+ - **Deep imports register what they render.** `u-prompt` renders its input as `<u-text-block>` and `u-table-block`
25
+ its search box as `<u-input>`, but each imported the class for a type only, so the build dropped both imports:
26
+ importing either component by its own module path left the prompt without an input and the table without a
27
+ search box. The package entry hid it (it registers every component).
28
+ - **`u-table-block`'s search box has a name** (new locale key `searchLabel` — en *Search rows*, ko *행 검색*); it had
29
+ only its placeholder, which disappears as you type.
30
+
3
31
  ## [0.17.4] - 2026-10-07
4
32
 
5
33
  ### Fixed
@@ -40,7 +40,7 @@ var UCodeBlock = class UCodeBlock extends UElement {
40
40
  .value=${value}
41
41
  ></u-copy-button>
42
42
  </div>
43
-
43
+ <!-- html-sink: highlight.js output — it escapes the source text and adds only its own span markup -->
44
44
  <pre class="hljs">${unsafeHTML(hljs.highlight(value, { language: lang }).value)}</pre>
45
45
 
46
46
  <div hidden aria-hidden="true">
@@ -102,15 +102,16 @@ var UMarkedBlock = class UMarkedBlock extends UElement {
102
102
  * 반드시 `this.parser.defaults`(우리 렌더러가 병합된 설정)를 함께 넘긴다.
103
103
  */
104
104
  renderTable(token) {
105
- const cellHtml = (cell) => this.placeholder.restore(cell.tokens ? Parser.parseInline(cell.tokens, this.parser.defaults) : cell.text);
106
- const headers = token.header.map((h) => ({
107
- text: cellHtml(h),
108
- align: h.align
109
- }));
110
- const rows = token.rows.map((row) => row.map((cell) => ({
111
- text: cellHtml(cell),
112
- align: cell.align
113
- })));
105
+ const cell = (c) => {
106
+ const markup = this.placeholder.restore(c.tokens ? Parser.parseInline(c.tokens, this.parser.defaults) : c.text);
107
+ return {
108
+ text: new DOMParser().parseFromString(markup, "text/html").body.textContent ?? "",
109
+ html: markup,
110
+ align: c.align
111
+ };
112
+ };
113
+ const headers = token.header.map(cell);
114
+ const rows = token.rows.map((row) => row.map(cell));
114
115
  return UTableBlock.buildHTML({
115
116
  headers,
116
117
  rows
@@ -1,16 +1,20 @@
1
1
  import { UDataElement } from '../UDataElement.js';
2
2
  /**
3
- * 테이블 셀 데이터 타입
3
+ * 테이블 셀 데이터 타입.
4
4
  *
5
- * ⚠`text`는 렌더링 시 `unsafeHTML()`로 그대로 주입되는 **신뢰된 HTML 마크업**입니다
6
- * (일반 텍스트가 아닙니다) — `UMarkedBlock`이 마크다운 인라인 렌더러(자체 XSS 방어
7
- * 포함: 원시 HTML escape, `javascript:`/`data:` 링크 차단)를 거쳐 채우는 것을
8
- * 전제로 합니다. 이 컴포넌트를 직접 구동하는 소비자가 정화되지 않은 외부 문자열을
9
- * `text`에 넣으면 XSS로 이어질 수 있습니다 — 반드시 사전에 sanitize할 것.
5
+ * `text` 는 **글자**다 — 그대로 보이고, 검색·정렬·CSV/XLS 내려받기가 이것을 쓴다. 서식이 있는 셀(마크다운의
6
+ * 굵게·코드·링크)은 `html` 에 마크업을 따로 싣는다 — 그러면 화면은 `html` 을, 검색·정렬·내려받기는 여전히 `text` 를 쓴다.
7
+ * 종전에는 `text` 하나가 마크업이라 이름과 달리 글자를 HTML 로 그렸고(넣은 문자열에 따라 마크업 주입),
8
+ * 검색·정렬·내려받기가 태그까지 읽었다.
10
9
  */
11
10
  export interface TableCell {
12
- /** 셀에 표시할 (이미 안전 처리된) HTML 마크업 */
11
+ /** 셀의 글자 — 표시(`html` 이 없을 때) · 검색 · 정렬 · 내려받기 */
13
12
  text: string;
13
+ /**
14
+ * 셀을 서식과 함께 그릴 **신뢰된** HTML 마크업 — 화면에만 쓰인다. `UMarkedBlock` 이 자기 마크다운 렌더러
15
+ * (원시 HTML 은 글자로, 위험한 링크 프로토콜 차단)의 출력으로 채운다. 외부 문자열을 그대로 넣지 말 것.
16
+ */
17
+ html?: string;
14
18
  /** 셀 내용의 정렬 방식. 기본값은 "left" */
15
19
  align: "left" | "center" | "right" | null;
16
20
  }
@@ -37,12 +41,20 @@ export declare class UTableBlock extends UDataElement {
37
41
  private search;
38
42
  private searchCache?;
39
43
  private searchTimer;
44
+ /** 이 표가 공유 하이라이트에 더한 범위 — 다시 그릴 때·떼어질 때 걷는다. */
45
+ private searchRanges;
46
+ protected updated(): void;
47
+ disconnectedCallback(): void;
40
48
  render(): import('lit-html').TemplateResult<1>;
49
+ /** 셀 내용 — 서식 마크업(`html`)이 있으면 그것을, 없으면 글자(`text`)를 그린다. */
50
+ private cellContent;
41
51
  /**
42
- * 하이라이트 렌더링: search 기준으로 매칭되는 부분을 <mark>로 감쌉니다.
43
- * 내부적으로 안전하게 escape한 뒤 매칭을 위해 RegExp를 사용합니다.
52
+ * 검색어 하이라이트 — 본문 셀의 글자 노드에서 찾은 자리를 공유 하이라이트에 범위로 더한다(CSS Custom Highlight API).
53
+ * DOM 을 바꾸지 않는다: 종전처럼 마크업 문자열에 `<mark>` 를 끼우면 태그·엔티티 안의 글자까지 잘렸다
54
+ * (`&amp;` 에서 «amp» 를 찾으면 엔티티가 깨졌다).
44
55
  */
45
- private renderHighlightedText;
56
+ private paintSearch;
57
+ private clearSearchRanges;
46
58
  /**
47
59
  * 현재 검색어와 정렬 상태에 따라 필터링 및 정렬된 행 목록을 반환합니다.
48
60
  */
@@ -11,7 +11,24 @@ import "@iyulab/components/dist/components/icon/UIcon.js";
11
11
  import "@iyulab/components/dist/components/skeleton/USkeleton.js";
12
12
  import "@iyulab/components/dist/components/button/UButton.js";
13
13
  import { repeat } from "lit/directives/repeat.js";
14
+ import "@iyulab/components/dist/components/input/UInput.js";
14
15
  //#region src/components/blocks/UTableBlock.ts
16
+ /** 검색어 하이라이트의 등록 이름 — 스타일의 `::highlight(u-table-block-search)` 와 같다. */
17
+ var SEARCH_HIGHLIGHT = "u-table-block-search";
18
+ /**
19
+ * 모든 표가 나눠 쓰는 하이라이트 하나(등록은 문서 전역이다). 지원하지 않는 브라우저에서는 없다 — 그때는
20
+ * 검색이 행을 거르기만 하고 글자를 칠하지 않는다.
21
+ */
22
+ function searchHighlight() {
23
+ const registry = typeof CSS !== "undefined" ? CSS.highlights : void 0;
24
+ if (!registry || typeof Highlight === "undefined") return void 0;
25
+ let highlight = registry.get(SEARCH_HIGHLIGHT);
26
+ if (!highlight) {
27
+ highlight = new Highlight();
28
+ registry.set(SEARCH_HIGHLIGHT, highlight);
29
+ }
30
+ return highlight;
31
+ }
15
32
  var UTableBlock = class UTableBlock extends UDataElement {
16
33
  constructor(..._args) {
17
34
  super(..._args);
@@ -24,10 +41,18 @@ var UTableBlock = class UTableBlock extends UDataElement {
24
41
  };
25
42
  this.search = "";
26
43
  this.searchTimer = null;
44
+ this.searchRanges = [];
27
45
  }
28
46
  static {
29
47
  this.styles = [super.styles, styles];
30
48
  }
49
+ updated() {
50
+ this.paintSearch();
51
+ }
52
+ disconnectedCallback() {
53
+ super.disconnectedCallback();
54
+ this.clearSearchRanges();
55
+ }
31
56
  render() {
32
57
  const rows = this.getFilteredSortedRows();
33
58
  return html`
@@ -37,6 +62,7 @@ var UTableBlock = class UTableBlock extends UDataElement {
37
62
  class="toolbar-search"
38
63
  type="search"
39
64
  placeholder=${messages.text("search")}
65
+ aria-label=${messages.text("searchLabel")}
40
66
  .value=${this.search}
41
67
  @input=${this.handleSearchInput}
42
68
  >
@@ -75,7 +101,7 @@ var UTableBlock = class UTableBlock extends UDataElement {
75
101
  <!-- 정렬은 머리 칸 전체를 채우는 버튼이다 — 클릭만 받는 th 는 키보드로 닿지 않았다. -->
76
102
  <button type="button" class="sort-button" part="sort-button"
77
103
  @click=${() => this.handleSortColumn(i)}>
78
- ${unsafeHTML(h.text)}
104
+ ${this.cellContent(h)}
79
105
  <u-icon
80
106
  class="sort-icon"
81
107
  lib="internal-chat"
@@ -98,7 +124,7 @@ var UTableBlock = class UTableBlock extends UDataElement {
98
124
  <tr>
99
125
  ${repeat(row, (_, j) => j, (cell) => html`
100
126
  <td align=${cell.align ?? "left"}>
101
- ${unsafeHTML(this.renderHighlightedText(cell.text))}
127
+ ${this.cellContent(cell)}
102
128
  </td>
103
129
  `)}
104
130
  </tr>
@@ -108,18 +134,38 @@ var UTableBlock = class UTableBlock extends UDataElement {
108
134
  </div>
109
135
  `;
110
136
  }
137
+ /** 셀 내용 — 서식 마크업(`html`)이 있으면 그것을, 없으면 글자(`text`)를 그린다. */
138
+ cellContent(cell) {
139
+ return cell.html != null ? unsafeHTML(cell.html) : cell.text;
140
+ }
111
141
  /**
112
- * 하이라이트 렌더링: search 기준으로 매칭되는 부분을 <mark>로 감쌉니다.
113
- * 내부적으로 안전하게 escape한 뒤 매칭을 위해 RegExp를 사용합니다.
142
+ * 검색어 하이라이트 — 본문 셀의 글자 노드에서 찾은 자리를 공유 하이라이트에 범위로 더한다(CSS Custom Highlight API).
143
+ * DOM 을 바꾸지 않는다: 종전처럼 마크업 문자열에 `<mark>` 를 끼우면 태그·엔티티 안의 글자까지 잘렸다
144
+ * (`&amp;` 에서 «amp» 를 찾으면 엔티티가 깨졌다).
114
145
  */
115
- renderHighlightedText(text) {
116
- const q = this.search.trim();
117
- if (!q) return text;
118
- const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
119
- const re = new RegExp(`(${escaped})`, "gi");
120
- const parts = String(text).split(re);
121
- if (parts.length === 1) return text;
122
- return parts.map((p, idx) => idx % 2 === 1 ? `<mark>${p}</mark>` : p).join("");
146
+ paintSearch() {
147
+ this.clearSearchRanges();
148
+ const highlight = searchHighlight();
149
+ const q = this.search.trim().toLowerCase();
150
+ const body = this.shadowRoot?.querySelector("tbody");
151
+ if (!highlight || !q || !body) return;
152
+ const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT);
153
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
154
+ const text = node.data.toLowerCase();
155
+ if (text.length !== node.data.length) continue;
156
+ for (let at = text.indexOf(q); at >= 0; at = text.indexOf(q, at + q.length)) {
157
+ const range = new Range();
158
+ range.setStart(node, at);
159
+ range.setEnd(node, at + q.length);
160
+ highlight.add(range);
161
+ this.searchRanges.push(range);
162
+ }
163
+ }
164
+ }
165
+ clearSearchRanges() {
166
+ const highlight = searchHighlight();
167
+ if (highlight) for (const range of this.searchRanges) highlight.delete(range);
168
+ this.searchRanges = [];
123
169
  }
124
170
  /**
125
171
  * 현재 검색어와 정렬 상태에 따라 필터링 및 정렬된 행 목록을 반환합니다.
@@ -14,6 +14,12 @@ var styles = css`
14
14
  overflow: hidden;
15
15
  }
16
16
 
17
+ /* 검색어 하이라이트(UTableBlock 의 공유 Highlight) — 종전 <mark> 와 같은 시스템 색. */
18
+ ::highlight(u-table-block-search) {
19
+ background-color: Mark;
20
+ color: MarkText;
21
+ }
22
+
17
23
  .toolbar {
18
24
  flex: none;
19
25
  display: flex;
@@ -3,6 +3,7 @@ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.152.0/hel
3
3
  import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
4
4
  import { messages } from "../../utilities/messages.js";
5
5
  import "../blocks/UFileBlock.js";
6
+ import "../blocks/UTextBlock.js";
6
7
  import { styles } from "./UPrompt.styles.js";
7
8
  import { html } from "lit";
8
9
  import { customElement, property } from "lit/decorators.js";
@@ -5,5 +5,5 @@
5
5
  * 이 리포가 채택한 표준이 *"영어 기본 **+ 레지스트리**"* 이기 때문이다 — 영어 리터럴만으로는
6
6
  * 한국어 앱이 그 문구를 되돌릴 방법이 없다. 다음 문자열은 여기로 온다.
7
7
  */
8
- export type ChatMessageKey = 'canvasUnavailable' | 'previousReference' | 'nextReference' | 'closePreview' | 'closeLightbox' | 'previousImage' | 'nextImage' | 'sendMessage' | 'stopGenerating' | 'fullScreen' | 'remove' | 'pngDownload' | 'jsonDownload' | 'search' | 'rowCount' | 'excelDownload' | 'csvDownload' | 'openImage' | 'previewFile' | 'imageBlocked' | 'imageBlockedNamed' | 'opensInNewTab' | 'textBlock' | 'references';
8
+ export type ChatMessageKey = 'canvasUnavailable' | 'previousReference' | 'nextReference' | 'closePreview' | 'closeLightbox' | 'previousImage' | 'nextImage' | 'sendMessage' | 'stopGenerating' | 'fullScreen' | 'remove' | 'pngDownload' | 'jsonDownload' | 'search' | 'searchLabel' | 'rowCount' | 'excelDownload' | 'csvDownload' | 'openImage' | 'previewFile' | 'imageBlocked' | 'imageBlockedNamed' | 'opensInNewTab' | 'textBlock' | 'references';
9
9
  export declare const messages: import('@iyulab/components/dist/utilities/Locale.js').LocaleNamespace<ChatMessageKey>;
@@ -16,6 +16,7 @@ messages.register("en", {
16
16
  pngDownload: "PNG Download",
17
17
  jsonDownload: "JSON Download",
18
18
  search: "Search...",
19
+ searchLabel: "Search rows",
19
20
  textBlock: "Text",
20
21
  references: "References",
21
22
  rowCount: "{shown} / {total} Rows",
@@ -42,6 +43,7 @@ messages.register("ko", {
42
43
  pngDownload: "PNG 다운로드",
43
44
  jsonDownload: "JSON 다운로드",
44
45
  search: "검색...",
46
+ searchLabel: "행 검색",
45
47
  textBlock: "텍스트",
46
48
  references: "참조",
47
49
  rowCount: "{shown} / {total} 행",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/chat-components",
3
3
  "description": "llm chat components for building chat interfaces",
4
- "version": "0.17.4",
4
+ "version": "0.18.0",
5
5
  "author": "iyulab",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -56,7 +56,7 @@ There is no size property or custom property for this — the host is the lever.
56
56
 
57
57
  | Property | Type | Default | Description |
58
58
  |----------|------|---------|-------------|
59
- | `headers` | `TableCell[]` | `[]` | Header cells, each with `text` and `align` |
59
+ | `headers` | `TableCell[]` | `[]` | Header cells, each with `text`, optional `html` and `align` |
60
60
  | `rows` | `TableCell[][]` | `[]` | Row data; each row is an array of `TableCell` |
61
61
 
62
62
  ## CSS Parts
@@ -70,15 +70,21 @@ There is no size property or custom property for this — the host is the lever.
70
70
  ```ts
71
71
  interface TableCell {
72
72
  text: string;
73
+ html?: string;
73
74
  align: 'left' | 'center' | 'right' | null;
74
75
  }
75
76
  ```
76
77
 
78
+ `text` is plain text: it is shown as written (markup in it appears as characters), and search, sort and the
79
+ CSV/XLS downloads use it. For a formatted cell, also give `html` — trusted markup that only the screen uses; search,
80
+ sort and downloads still read `text`. `u-marked-block` fills both from its markdown renderer. Never put an untrusted
81
+ string in `html`.
82
+
77
83
  ## Features
78
84
 
79
85
  | Feature | Description |
80
86
  |---------|-------------|
81
87
  | Column sort | Click a header to sort asc/desc |
82
- | Search filter | Search bar filters rows (debounced) |
88
+ | Search filter | Search bar filters rows by `text` (debounced) and highlights the matches without changing the cell markup (CSS Custom Highlight API; no highlight where the browser lacks it) |
83
89
  | CSV download | Downloads current filtered/sorted data as CSV |
84
90
  | XLS download | Downloads as Excel file |