@catandbox/schrodinger-web-adapter 0.1.16 → 0.1.18

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.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * DOM helpers that avoid direct innerHTML/outerHTML assignment.
3
+ * All dynamic content must still be escaped before passing to these functions.
4
+ */
5
+ export declare function setInnerHtml(el: HTMLElement, html: string): void;
6
+ export declare function replaceOuterHtml(el: HTMLElement, html: string): void;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * DOM helpers that avoid direct innerHTML/outerHTML assignment.
3
+ * All dynamic content must still be escaped before passing to these functions.
4
+ */
5
+ export function setInnerHtml(el, html) {
6
+ el.replaceChildren();
7
+ if (html) {
8
+ const frag = document.createRange().createContextualFragment(html);
9
+ el.append(frag);
10
+ }
11
+ }
12
+ export function replaceOuterHtml(el, html) {
13
+ const frag = document.createRange().createContextualFragment(html);
14
+ el.replaceWith(frag);
15
+ }
@@ -1,3 +1,4 @@
1
+ import { setInnerHtml, replaceOuterHtml } from "./dom-utils";
1
2
  const MAX_FILES = 5;
2
3
  const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
3
4
  async function computeSha256(file) {
@@ -85,7 +86,7 @@ export function createFileUpload() {
85
86
  const errorEl = container.querySelector("#sch-file-error");
86
87
  const err = addFiles(input.files);
87
88
  if (err) {
88
- errorEl.innerHTML = `<s-text variant="bodySm" tone="critical">${escapeHtml(err)}</s-text>`;
89
+ setInnerHtml(errorEl, `<s-text variant="bodySm" tone="critical">${escapeHtml(err)}</s-text>`);
89
90
  }
90
91
  else {
91
92
  errorEl.innerHTML = "";
@@ -108,7 +109,7 @@ export function createFileUpload() {
108
109
  const wrapper = container.querySelector("#sch-file-list")?.parentElement;
109
110
  if (!wrapper)
110
111
  return;
111
- wrapper.outerHTML = render();
112
+ replaceOuterHtml(wrapper, render());
112
113
  attachListeners(container);
113
114
  }
114
115
  function hasFiles() {
@@ -1,4 +1,5 @@
1
1
  import { createFileUpload } from "./file-upload";
2
+ import { setInnerHtml } from "./dom-utils";
2
3
  const FIELD_STYLE = [
3
4
  "width:100%",
4
5
  "padding:10px 12px",
@@ -18,7 +19,7 @@ export function renderNewTicketForm(container, client, categories, emitter) {
18
19
  .map((cat) => `<option value="${cat.id}">${escapeHtml(cat.name)}</option>`)
19
20
  .join("");
20
21
  const fileUpload = createFileUpload();
21
- container.innerHTML = `
22
+ setInnerHtml(container, `
22
23
  <s-card>
23
24
  <s-box padding="large">
24
25
  <s-stack gap="large">
@@ -82,7 +83,7 @@ export function renderNewTicketForm(container, client, categories, emitter) {
82
83
  </s-stack>
83
84
  </s-box>
84
85
  </s-card>
85
- `;
86
+ `);
86
87
  fileUpload.attachListeners(container);
87
88
  const form = container.querySelector("#sch-new-ticket-form");
88
89
  container.querySelector("#sch-cancel-btn")?.addEventListener("click", () => {
@@ -131,11 +132,11 @@ export function renderNewTicketForm(container, client, categories, emitter) {
131
132
  emitter.emit("ticket:created", ticket.id);
132
133
  }
133
134
  catch (error) {
134
- errorEl.innerHTML = `
135
+ setInnerHtml(errorEl, `
135
136
  <s-banner tone="critical">
136
137
  <s-text>Failed to create ticket: ${escapeHtml(error instanceof Error ? error.message : String(error))}</s-text>
137
138
  </s-banner>
138
- `;
139
+ `);
139
140
  submitBtn.removeAttribute("disabled");
140
141
  }
141
142
  });
@@ -1,5 +1,6 @@
1
1
  import { renderStatusBadge } from "./status-badge";
2
2
  import { createFileUpload } from "./file-upload";
3
+ import { setInnerHtml } from "./dom-utils";
3
4
  function formatTimestamp(timestamp) {
4
5
  const date = new Date(timestamp * 1000);
5
6
  const now = new Date();
@@ -37,7 +38,7 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
37
38
  const data = await client.getTicket(ticketId);
38
39
  const { ticket, messages } = data;
39
40
  const isOpen = ticket.status !== "Closed" && ticket.status !== "Archived";
40
- container.innerHTML = `
41
+ setInnerHtml(container, `
41
42
  <s-card>
42
43
  <s-box padding="large">
43
44
  <s-stack gap="large">
@@ -103,7 +104,7 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
103
104
  </s-box>
104
105
  </s-card>`
105
106
  : ""}
106
- `;
107
+ `);
107
108
  // Back button
108
109
  container.querySelector("#sch-back-btn")?.addEventListener("click", () => {
109
110
  emitter.emit("ticket:back", undefined);
@@ -113,7 +114,7 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
113
114
  const replyFilesContainer = container.querySelector("#sch-reply-files");
114
115
  if (replyFilesContainer) {
115
116
  replyFileUpload = createFileUpload();
116
- replyFilesContainer.innerHTML = replyFileUpload.render();
117
+ setInnerHtml(replyFilesContainer, replyFileUpload.render());
117
118
  replyFileUpload.attachListeners(replyFilesContainer);
118
119
  }
119
120
  // Send reply
@@ -168,7 +169,7 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
168
169
  });
169
170
  }
170
171
  catch (error) {
171
- container.innerHTML = `
172
+ setInnerHtml(container, `
172
173
  <s-card>
173
174
  <s-box padding="large">
174
175
  <s-stack gap="base">
@@ -184,7 +185,7 @@ export async function renderTicketDetail(container, client, ticketId, emitter) {
184
185
  </s-stack>
185
186
  </s-box>
186
187
  </s-card>
187
- `;
188
+ `);
188
189
  container.querySelector("#sch-back-btn")?.addEventListener("click", () => {
189
190
  emitter.emit("ticket:back", undefined);
190
191
  });
@@ -237,11 +238,11 @@ function showError(container, message) {
237
238
  existing.remove();
238
239
  const banner = document.createElement("div");
239
240
  banner.id = "sch-error-banner";
240
- banner.innerHTML = `
241
+ setInnerHtml(banner, `
241
242
  <s-banner tone="critical" style="margin-bottom:12px;">
242
243
  <s-text>${escapeHtml(message)}</s-text>
243
244
  </s-banner>
244
- `;
245
+ `);
245
246
  const replySection = container.querySelector("#sch-reply-section");
246
247
  if (replySection) {
247
248
  replySection.insertBefore(banner, replySection.firstChild);
@@ -1,4 +1,5 @@
1
1
  import { renderStatusBadge } from "./status-badge";
2
+ import { setInnerHtml } from "./dom-utils";
2
3
  function formatDate(timestamp) {
3
4
  return new Date(timestamp * 1000).toLocaleDateString(undefined, {
4
5
  month: "short",
@@ -75,7 +76,7 @@ export async function renderTicketList(container, client, emitter, statusFilter)
75
76
  ?.addEventListener("click", () => emitter.emit("ticket:create", undefined));
76
77
  return;
77
78
  }
78
- body.innerHTML = tickets
79
+ setInnerHtml(body, tickets
79
80
  .map((ticket) => `
80
81
  <div class="sch-ticket-row" data-ticket-id="${ticket.id}"
81
82
  style="display:flex; justify-content:space-between; align-items:center; padding:14px 16px; margin:0 -16px; border-radius:8px; cursor:pointer; transition:background 0.15s ease;"
@@ -100,7 +101,7 @@ export async function renderTicketList(container, client, emitter, statusFilter)
100
101
  </div>
101
102
  </div>
102
103
  `)
103
- .join(`<div style="border-bottom:1px solid var(--p-color-border-subdued, #e1e3e5); margin:0;"></div>`);
104
+ .join(`<div style="border-bottom:1px solid var(--p-color-border-subdued, #e1e3e5); margin:0;"></div>`));
104
105
  container.querySelectorAll("[data-ticket-id]").forEach((el) => {
105
106
  el.addEventListener("click", () => {
106
107
  const ticketId = el.getAttribute("data-ticket-id");
@@ -111,11 +112,11 @@ export async function renderTicketList(container, client, emitter, statusFilter)
111
112
  });
112
113
  }
113
114
  catch (error) {
114
- body.innerHTML = `
115
+ setInnerHtml(body, `
115
116
  <s-banner tone="critical">
116
117
  <s-text>Failed to load tickets: ${escapeHtml(error instanceof Error ? error.message : String(error))}</s-text>
117
118
  </s-banner>
118
- `;
119
+ `);
119
120
  }
120
121
  }
121
122
  function getStatusDotColor(status) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catandbox/schrodinger-web-adapter",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",