@iyulab/chat-components 0.17.5 → 0.18.1
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 +25 -0
- package/dist/components/blocks/UCodeBlock.js +1 -1
- package/dist/components/blocks/UMarkedBlock.js +10 -9
- package/dist/components/blocks/UTableBlock.d.ts +22 -10
- package/dist/components/blocks/UTableBlock.js +56 -12
- package/dist/components/blocks/UTableBlock.styles.js +6 -0
- package/package.json +1 -1
- package/skills/iyulab-chat-components/references/components/table-block.md +8 -2
- package/skills/iyulab-chat-components/references/security.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.18.1] - 2026-10-07
|
|
4
|
+
|
|
5
|
+
### Documentation
|
|
6
|
+
|
|
7
|
+
- The security guide still listed `u-table-block` cell `text` as trusted HTML. It now lists `text` as untrusted
|
|
8
|
+
(rendered as text) and the new `html` field as the trusted-markup input, as 0.18.0 changed — in `docs/security.md`
|
|
9
|
+
and the skill security reference.
|
|
10
|
+
|
|
11
|
+
## [0.18.0] - 2026-10-07
|
|
12
|
+
|
|
13
|
+
### Changed (breaking)
|
|
14
|
+
|
|
15
|
+
- **`u-table-block` cell `text` is plain text.** It was rendered as HTML, so a string meant as text — a name, a
|
|
16
|
+
server value — injected markup, and search, sort and the CSV/XLS downloads read the tags too (a bold `9` sorted
|
|
17
|
+
after `10`; searching `strong` matched every bold cell; downloads carried `<strong>`). A formatted cell now gives
|
|
18
|
+
its markup in the new optional `html` field, used only for display. `u-marked-block` fills both, so markdown
|
|
19
|
+
tables look the same. **Migration:** if you passed markup in `text`, move it to `html` and put the plain text in
|
|
20
|
+
`text`.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **Search highlighting no longer cuts markup.** It inserted `<mark>` into the cell's HTML string, splitting tags and
|
|
25
|
+
entities (searching `amp` broke `&`). Matches are now painted with the CSS Custom Highlight API over the
|
|
26
|
+
rendered text; the DOM is unchanged.
|
|
27
|
+
|
|
3
28
|
## [0.17.5] - 2026-10-07
|
|
4
29
|
|
|
5
30
|
### 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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
* 하이라이트
|
|
43
|
-
*
|
|
52
|
+
* 검색어 하이라이트 — 본문 셀의 글자 노드에서 찾은 자리를 공유 하이라이트에 범위로 더한다(CSS Custom Highlight API).
|
|
53
|
+
* DOM 을 바꾸지 않는다: 종전처럼 마크업 문자열에 `<mark>` 를 끼우면 태그·엔티티 안의 글자까지 잘렸다
|
|
54
|
+
* (`&` 에서 «amp» 를 찾으면 엔티티가 깨졌다).
|
|
44
55
|
*/
|
|
45
|
-
private
|
|
56
|
+
private paintSearch;
|
|
57
|
+
private clearSearchRanges;
|
|
46
58
|
/**
|
|
47
59
|
* 현재 검색어와 정렬 상태에 따라 필터링 및 정렬된 행 목록을 반환합니다.
|
|
48
60
|
*/
|
|
@@ -13,6 +13,22 @@ import "@iyulab/components/dist/components/button/UButton.js";
|
|
|
13
13
|
import { repeat } from "lit/directives/repeat.js";
|
|
14
14
|
import "@iyulab/components/dist/components/input/UInput.js";
|
|
15
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
|
+
}
|
|
16
32
|
var UTableBlock = class UTableBlock extends UDataElement {
|
|
17
33
|
constructor(..._args) {
|
|
18
34
|
super(..._args);
|
|
@@ -25,10 +41,18 @@ var UTableBlock = class UTableBlock extends UDataElement {
|
|
|
25
41
|
};
|
|
26
42
|
this.search = "";
|
|
27
43
|
this.searchTimer = null;
|
|
44
|
+
this.searchRanges = [];
|
|
28
45
|
}
|
|
29
46
|
static {
|
|
30
47
|
this.styles = [super.styles, styles];
|
|
31
48
|
}
|
|
49
|
+
updated() {
|
|
50
|
+
this.paintSearch();
|
|
51
|
+
}
|
|
52
|
+
disconnectedCallback() {
|
|
53
|
+
super.disconnectedCallback();
|
|
54
|
+
this.clearSearchRanges();
|
|
55
|
+
}
|
|
32
56
|
render() {
|
|
33
57
|
const rows = this.getFilteredSortedRows();
|
|
34
58
|
return html`
|
|
@@ -77,7 +101,7 @@ var UTableBlock = class UTableBlock extends UDataElement {
|
|
|
77
101
|
<!-- 정렬은 머리 칸 전체를 채우는 버튼이다 — 클릭만 받는 th 는 키보드로 닿지 않았다. -->
|
|
78
102
|
<button type="button" class="sort-button" part="sort-button"
|
|
79
103
|
@click=${() => this.handleSortColumn(i)}>
|
|
80
|
-
${
|
|
104
|
+
${this.cellContent(h)}
|
|
81
105
|
<u-icon
|
|
82
106
|
class="sort-icon"
|
|
83
107
|
lib="internal-chat"
|
|
@@ -100,7 +124,7 @@ var UTableBlock = class UTableBlock extends UDataElement {
|
|
|
100
124
|
<tr>
|
|
101
125
|
${repeat(row, (_, j) => j, (cell) => html`
|
|
102
126
|
<td align=${cell.align ?? "left"}>
|
|
103
|
-
${
|
|
127
|
+
${this.cellContent(cell)}
|
|
104
128
|
</td>
|
|
105
129
|
`)}
|
|
106
130
|
</tr>
|
|
@@ -110,18 +134,38 @@ var UTableBlock = class UTableBlock extends UDataElement {
|
|
|
110
134
|
</div>
|
|
111
135
|
`;
|
|
112
136
|
}
|
|
137
|
+
/** 셀 내용 — 서식 마크업(`html`)이 있으면 그것을, 없으면 글자(`text`)를 그린다. */
|
|
138
|
+
cellContent(cell) {
|
|
139
|
+
return cell.html != null ? unsafeHTML(cell.html) : cell.text;
|
|
140
|
+
}
|
|
113
141
|
/**
|
|
114
|
-
* 하이라이트
|
|
115
|
-
*
|
|
142
|
+
* 검색어 하이라이트 — 본문 셀의 글자 노드에서 찾은 자리를 공유 하이라이트에 범위로 더한다(CSS Custom Highlight API).
|
|
143
|
+
* DOM 을 바꾸지 않는다: 종전처럼 마크업 문자열에 `<mark>` 를 끼우면 태그·엔티티 안의 글자까지 잘렸다
|
|
144
|
+
* (`&` 에서 «amp» 를 찾으면 엔티티가 깨졌다).
|
|
116
145
|
*/
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 = [];
|
|
125
169
|
}
|
|
126
170
|
/**
|
|
127
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;
|
package/package.json
CHANGED
|
@@ -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 |
|
|
@@ -8,7 +8,8 @@ is trusted to contain:
|
|
|
8
8
|
| `u-marked-block` `value` | untrusted | Raw HTML shown as text · link/image URLs protocol-checked (`javascript:`/`data:`/`vbscript:`/`//host` → `#`) · code fences escaped · table cells same rules |
|
|
9
9
|
| `block-json` fences | untrusted | Only registered blocks; only the schema's property names are assigned |
|
|
10
10
|
| `refs` | untrusted | Text rendered as text · URLs protocol-checked in `u-ref-tag` and `u-ref-card` |
|
|
11
|
-
| `u-table-block` cell `text` bound directly |
|
|
11
|
+
| `u-table-block` cell `text` bound directly | untrusted | Rendered as text; search, sort and downloads use it |
|
|
12
|
+
| `u-table-block` cell `html` bound directly | **trusted HTML** | Display only. Sanitize before binding. `block-json` cannot reach it |
|
|
12
13
|
| Your custom block | your element's contract | The schema allows names, not content — sanitize HTML-rendered properties inside the element |
|
|
13
14
|
|
|
14
15
|
Requests: markdown images and `u-images-block` load the `http:`/`https:` URL the model writes as soon as they
|