@seed-ship/mcp-ui-solid 5.6.0 → 5.7.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
@@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.7.0] - 2026-05-02
9
+
10
+ ### Added — citation chips inside table cells (opt-in)
11
+
12
+ Implements `mcp-ui-solid/docs/briefs/BRIEF-citations-in-table-cells.md`. Lifts the chip-rendering responsibility out of consumer apps (deposium had a server-side bridge in `deposium_MCPs` commit `7df433ae` that this obsoletes) so any host stops mirroring chip HTML byte-for-byte.
13
+
14
+ - **`TableComponentParams.citationMap`** (optional, JSON-serializable) — `Record<string|number, { page, file?, file_id? }>`. When present, `<TableRenderer>` walks each cell string and replaces LLM `[N]` / `Citation [N]` / `[CITATION N]` / `[📄 CITATION N]` markers with chip HTML carrying `data-citation-page` + `data-citation-doc` + `data-citation-verified` attrs. Hosts can route clicks via `target.closest('[data-citation-page]')` delegation — same path as inline-markdown chips.
15
+ - **`TableComponentParams.citationRender`** (optional, function — NOT JSON-serializable, consumer-wired) — override returning sanitized chip HTML for one marker. Wins over the default chip shape.
16
+ - **`renderCellValue(value, citationCtx?)`** — new optional 2nd arg. Standalone use (outside `<TableRenderer>`) supported : same opt-in behavior, same DOMPurify whitelist guarantees.
17
+ - **`CitationCtx`** + **`CitationEntry`** types exported from the package root.
18
+ - **`defaultCitationChip()`** uses neutral Tailwind classes (`bg-gray-800 text-gray-500 border-gray-600 hover:border-teal-500`) layered with the `.citation-ref` CSS class — hosts already styling `.citation-ref` for their inline chips get visual consistency for free, no per-table override needed.
19
+
20
+ Markdown composition : cells like `**MSP** [1]` produce `<strong>MSP</strong>` AND a chip in the same cell. The hasMarkdown / hasHtml branches in `renderCellValue` were re-ordered + DOMPurify whitelists extended so chip HTML survives both paths.
21
+
22
+ Resolution rules :
23
+ - Resolved id (in map) → default chip shape `[file - page]` + button with citation attrs.
24
+ - Unresolved id with **non-empty** map → marker dropped silently (likely LLM hallucination, mirrors typical host behavior).
25
+ - Unresolved id with **empty** map → human-visible `[réf. N]` placeholder.
26
+ - `[p.5]` page form → preserved (negative lookbehind).
27
+ - `[text](url)` markdown link → preserved (existing branch runs first).
28
+
29
+ CSV export of cells with citations : raw markers (`[1]`) flow through unchanged. The chip HTML is only injected at render time; the CSV path uses the original `row[key]` value, which is the right choice for re-importable exports.
30
+
31
+ ### Changed
32
+
33
+ - Dep bump : `@seed-ship/mcp-ui-spec` `^5.0.2` → `^5.0.3` (adds `CitationEntrySchema` + `TableComponentParamsSchema.citationMap` for cross-stack type safety).
34
+
35
+ ### Tests
36
+
37
+ - `src/components/TableRenderer.citation.test.tsx` — **+15 tests** covering : no-ctx regression, single + multi chip emission, unresolved id (non-empty + empty map paths), `citationRender` override, `[p.5]` skip, markdown-link skip, mixed `**bold** [1]` compose, canonical marker shortcut, DOMPurify attr survival, button element check, plus 3 integration tests on a real `<UIResourceRenderer>` mount (no map → plain text, with map → chips in DOM, render override → custom chips).
38
+ - Existing 545/545 tests untouched, all still pass.
39
+ - Total solid suite : **560/560 tests pass** (vs 545 on v5.6.0, +15 net).
40
+
41
+ ### Migration
42
+
43
+ - 100% backward compatible. `citationMap` not set → cells render exactly as before.
44
+ - Hosts opt in by adding `citationMap: gaResult.citation_map` to their table params.
45
+ - deposium_MCPs can now revert commit `7df433ae` (server-side `renderCitationChipHTML` + `replaceCitationsInCellHTML` helpers) and emit raw `[📄 CITATION N]` markers in cells with `params.citationMap` set — chip rendering happens client-side.
46
+
8
47
  ## [5.6.0] - 2026-04-27
9
48
 
10
49
  Closes B.1 migration (14/17 ComponentTypes spec-driven) AND ships B.5 — UI
@@ -221,7 +221,27 @@ function highlightQuery(html, query) {
221
221
  return text.replace(regex, '<mark class="bg-yellow-200 dark:bg-[#222F49] text-inherit rounded px-0.5">$1</mark>');
222
222
  });
223
223
  }
224
- function renderCellValue(value) {
224
+ function defaultCitationChip(pageNum, fileName, verified = true) {
225
+ const safeDocName = encodeURIComponent(fileName || "");
226
+ const label = fileName ? `${fileName} - ${pageNum}` : `${pageNum}`;
227
+ if (!verified) {
228
+ return `<span class="citation-ref inline-flex items-center gap-0.5 align-middle opacity-60"><span class="text-gray-500 line-through">[${label}]</span></span>`;
229
+ }
230
+ return ['<span class="citation-ref inline-flex items-center gap-0.5 align-middle">', `<span class="text-gray-500">[${label}]</span>`, '<button class="inline-flex items-center ml-0.5 px-1 py-0.5 text-xs bg-gray-800 hover:bg-gray-700 border border-gray-600 hover:border-teal-500 rounded cursor-pointer transition-colors align-middle"', ` data-citation-page="${pageNum}"`, ` data-citation-doc="${safeDocName}"`, ' data-citation-verified="true"', ` title="View source - ${label}">`, '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>', "</button>", "</span>"].join("");
231
+ }
232
+ function transformCellCitations(text, ctx) {
233
+ let out = text.replace(new RegExp("(?<![p.])\\[(\\d{1,2})\\](?!\\()", "g"), "[📄 CITATION $1]");
234
+ out = out.replace(/\bCitations?\s*\[(\d+)\]/gi, "[📄 CITATION $1]");
235
+ out = out.replace(/\[CITATION\s+(\d+)\]/gi, "[📄 CITATION $1]");
236
+ return out.replace(/[【[]\s*📄\s*CITATION\s*(\d+)\s*[】\]]/gi, (_m, idStr) => {
237
+ const id = parseInt(idStr, 10);
238
+ const mapping = ctx.map[id] ?? ctx.map[String(id)];
239
+ if (ctx.render) return ctx.render(id, mapping);
240
+ if (mapping) return defaultCitationChip(mapping.page, mapping.file ?? "", true);
241
+ return Object.keys(ctx.map).length > 0 ? "" : `[réf. ${id}]`;
242
+ });
243
+ }
244
+ function renderCellValue(value, citationCtx) {
225
245
  if (value === null || value === void 0) {
226
246
  return "-";
227
247
  }
@@ -257,20 +277,25 @@ function renderCellValue(value) {
257
277
  ADD_ATTR: ["target", "rel"]
258
278
  });
259
279
  }
260
- const hasHtml = /<[a-z][\s\S]*>/i.test(strValue);
261
- if (hasHtml) {
262
- return purify_es.sanitize(strValue, {
263
- ALLOWED_TAGS: ["a", "strong", "em", "b", "i", "code", "span", "br", "button", "svg", "path"],
264
- ALLOWED_ATTR: ["href", "target", "rel", "class", "data-citation-page", "data-citation-source", "data-citation-doc", "data-citation-verified", "title", "fill", "stroke", "viewBox", "stroke-linecap", "stroke-linejoin", "stroke-width", "d"],
265
- ADD_ATTR: ["target", "rel"]
266
- });
280
+ if (citationCtx) {
281
+ strValue = transformCellCitations(strValue, citationCtx);
267
282
  }
268
- const hasMarkdown = /[*_`[\]#]/.test(strValue);
283
+ const hasMarkdown = /[*_`#]/.test(strValue);
269
284
  if (hasMarkdown) {
270
285
  const parsed = marked_esm.marked.parse(strValue, {
271
286
  async: false
272
287
  });
273
288
  return purify_es.sanitize(parsed, {
289
+ ALLOWED_TAGS: ["a", "strong", "em", "b", "i", "code", "span", "br", "button", "svg", "path", "p", "ul", "ol", "li", "pre", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6"],
290
+ ALLOWED_ATTR: ["href", "target", "rel", "class", "data-citation-page", "data-citation-source", "data-citation-doc", "data-citation-verified", "title", "fill", "stroke", "viewBox", "stroke-linecap", "stroke-linejoin", "stroke-width", "d"],
291
+ ADD_ATTR: ["target", "rel"]
292
+ });
293
+ }
294
+ const hasHtml = /<[a-z][\s\S]*>/i.test(strValue);
295
+ if (hasHtml) {
296
+ return purify_es.sanitize(strValue, {
297
+ ALLOWED_TAGS: ["a", "strong", "em", "b", "i", "code", "span", "br", "button", "svg", "path"],
298
+ ALLOWED_ATTR: ["href", "target", "rel", "class", "data-citation-page", "data-citation-source", "data-citation-doc", "data-citation-verified", "title", "fill", "stroke", "viewBox", "stroke-linecap", "stroke-linejoin", "stroke-width", "d"],
274
299
  ADD_ATTR: ["target", "rel"]
275
300
  });
276
301
  }
@@ -279,6 +304,10 @@ function renderCellValue(value) {
279
304
  function TableRenderer(props) {
280
305
  const tableParams = props.component.params;
281
306
  let scrollContainerRef;
307
+ const citationCtx = tableParams.citationMap ? {
308
+ map: tableParams.citationMap,
309
+ render: tableParams.citationRender
310
+ } : void 0;
282
311
  const allRows = () => tableParams.rows || [];
283
312
  const columns = () => tableParams.columns || [];
284
313
  const [sortKey, setSortKey] = solidJs.createSignal(null);
@@ -519,7 +548,7 @@ ${dataRows}`;
519
548
  },
520
549
  children: (column) => (() => {
521
550
  var _el$23 = web.getNextElement(_tmpl$10), _el$24 = _el$23.firstChild;
522
- web.effect(() => web.setProperty(_el$24, "innerHTML", highlightQuery(renderCellValue(row[column.key]), debouncedQuery())));
551
+ web.effect(() => web.setProperty(_el$24, "innerHTML", highlightQuery(renderCellValue(row[column.key], citationCtx), debouncedQuery())));
523
552
  return _el$23;
524
553
  })()
525
554
  }));
@@ -552,7 +581,7 @@ ${dataRows}`;
552
581
  },
553
582
  children: (column) => (() => {
554
583
  var _el$27 = web.getNextElement(_tmpl$10), _el$28 = _el$27.firstChild;
555
- web.effect(() => web.setProperty(_el$28, "innerHTML", highlightQuery(renderCellValue(row[column.key]), debouncedQuery())));
584
+ web.effect(() => web.setProperty(_el$28, "innerHTML", highlightQuery(renderCellValue(row[column.key], citationCtx), debouncedQuery())));
556
585
  return _el$27;
557
586
  })()
558
587
  }));