@adia-ai/web-modules 0.8.3 → 0.8.5

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,4 +1,28 @@
1
1
  # Changelog — @adia-ai/web-modules
2
+
3
+ ## [0.8.5] — 2026-07-17
4
+
5
+ ### Added
6
+ - **`scripts/verify/exports-wildcard-resolution.mjs`** (gh#296, shared with `@adia-ai/web-components`): live-resolution regression gate asserting every documented short-form CSS/JS subpath specifier resolves to a real on-disk file. Removed 6 dead exports-map keys (`./shell/*/*.css`, `./chat/*/*.css`, `./editor/*/*.css`, `./simple/*/*.css`, `./runtime/*/*.css`, `./theme/*/*.css`) — confirmed via live `import.meta.resolve()` testing that these never actually resolved (Node's pattern matching always picked the single-wildcard sibling key instead), so they provided no real "nested form" support despite looking like they did. Documented the underlying trap (a redundant nested specifier like `shell/admin-command/admin-command.css` silently resolves to a garbage, nonexistent quadrupled path instead of throwing) in `README.md` — not fixable in the exports map itself, since Node's `*` matches across `/` with no way to constrain it to one path segment.
7
+
8
+ ### Fixed
9
+ - **`shell/admin-sidebar`: a zero-rect connect no longer snaps the sidebar collapsed (gh#286)**: `connectedCallback` read `getBoundingClientRect().width` synchronously and derived `[collapsed]` from it unconditionally — SSR shims (no layout engine) and any early-connect case (an ancestor `display:none`) always return 0×0, which unconditionally satisfied the collapse threshold. A zero width is now treated as "unknown," not "collapsed" — `#syncCollapsedFromWidth()` no-ops on a 0 read; the existing per-host `ResizeObserver` now also derives `[collapsed]` from its first tick as the deferred correction. 3 new tests cover the zero-rect connect, a persisted-width survival case, and the `ResizeObserver` self-correction in both directions.
10
+ - **Browser-only API call sites guarded for SSR (gh#285, shared with `@adia-ai/web-components`)**: `chat/chat-sidebar`, `chat/chat-thread`, `dashboard/dashboard-layout`, `editor/editor-canvas`, `editor/editor-sidebar`, and `shell/admin-sidebar` had unconditional `ResizeObserver`/`IntersectionObserver`/`MutationObserver` construction sites, now feature-detected — same audit and fix shape as `@adia-ai/web-components`; see `packages/web-components/CHANGELOG.md#085--2026-07-17` for the framework-wide detail.
11
+
12
+ ### Maintenance
13
+ - **`dist/` bundles rebuilt** (`chat/chat-shell.min.js`, `editor/editor-shell.min.js`, `shell/admin-shell.min.js`, `simple/simple-shell.min.js`, `everything.min.js`) — picks up the gh#285/gh#286 source fixes above.
14
+
15
+ ## [0.8.4] — 2026-07-16
16
+
17
+ ### Added
18
+ - **`chat-shell.appendMessage({ html: true })`** — assistant messages can carry trusted app-authored component markup (light-DOM upgrade in place); the `render` (markdown) path escapes inline HTML by design, which made gen-ui's error bubbles show raw `<col-ui …>` source text. The html mode persists on the message record and survives `stopStreaming()` and `conversation` getter/setter round-trips.
19
+
20
+ ### Fixed
21
+ - **`billing/payment-method-list` row focus ring double-drew the edge** — the focused row kept its 1px card border under the ring (`outline` + `outline-offset`); `border-color: transparent` while focused, per the framework-wide focus-ring pairing ruling (2026-07-16).
22
+
23
+ ### Maintenance
24
+ - **`dist/` bundles rebuilt** (`chat/chat-shell.min.js`, `everything.min.js` — the chat-shell change plus the inlined @adia-ai/llm provider-routing fixes).
25
+
2
26
  ## [0.8.3] — 2026-07-16
3
27
 
4
28
  ### Maintenance
package/README.md CHANGED
@@ -168,6 +168,17 @@ import '@adia-ai/web-modules/runtime/gen-root.css';
168
168
 
169
169
  > **For pre-v0.4.5 consumers:** if you were importing CSS via the relative `node_modules` path (`'../node_modules/@adia-ai/web-modules/editor/editor-shell/editor-shell.css'`), switch to the package-specifier form above. The relative-path form is fragile under pnpm, Yarn PnP, and npm hoisting; the new package-specifier form works under all three.
170
170
 
171
+ > **Don't repeat the directory name.** Every cluster child lives at
172
+ > `<cluster>/<name>/<name>.css` on disk (e.g. `shell/admin-command/admin-command.css`),
173
+ > so `@adia-ai/web-modules/shell/admin-command/admin-command.css` looks like
174
+ > the natural specifier — it is NOT. Use the short form,
175
+ > `@adia-ai/web-modules/shell/admin-command.css`, exactly as shown above.
176
+ > The nested form silently resolves to a garbage, nonexistent path
177
+ > (the directory name quadrupled) instead of a clear "not found" error —
178
+ > Node's exports-map wildcards match across `/` with no way to prevent
179
+ > this, so it can't be fixed in the package. Tracked as
180
+ > [gh#296](https://github.com/adiahealth/gen-ui-kit/issues/296).
181
+
171
182
  ## Import: JS + CSS in one line (`/with-css`)
172
183
 
173
184
  For the `shell` cluster, the `/with-css` opt-in collapses the JS + CSS
@@ -66,13 +66,15 @@ class ChatShell extends UIElement {
66
66
  get conversation() {
67
67
  return this.#messages
68
68
  .filter(m => m.role === 'user' || m.role === 'assistant')
69
- .map(m => ({ role: m.role, content: m.content }));
69
+ .map(m => (m.html ? { role: m.role, content: m.content, html: true } : { role: m.role, content: m.content }));
70
70
  }
71
71
 
72
72
  set conversation(msgs) {
73
73
  this.clear();
74
74
  for (const m of msgs) {
75
- this.appendMessage({ role: m.role, content: m.content, render: true });
75
+ // Round-trip the html mode re-rendering an html message through
76
+ // the markdown path would escape its component markup.
77
+ this.appendMessage({ role: m.role, content: m.content, render: !m.html, html: !!m.html });
76
78
  }
77
79
  }
78
80
 
@@ -122,9 +124,22 @@ class ChatShell extends UIElement {
122
124
 
123
125
  // ── Message management ──
124
126
 
125
- appendMessage({ role, content = '', render: renderMd = false }) {
127
+ /**
128
+ * @param {object} msg
129
+ * @param {string} msg.role — 'user' | 'assistant' | 'error'
130
+ * @param {string} [msg.content]
131
+ * @param {boolean} [msg.render] — treat content as MARKDOWN (inline HTML
132
+ * is escaped — component markup shows as literal text under this flag)
133
+ * @param {boolean} [msg.html] — assistant only: insert content as raw
134
+ * HTML (light-DOM component markup upgrades in place). TRUSTED
135
+ * app-authored strings only — never LLM output or user input; the
136
+ * caller owns escaping of any interpolated values.
137
+ */
138
+ appendMessage({ role, content = '', render: renderMd = false, html = false }) {
126
139
  const id = `msg_${++msgId}`;
127
- this.#messages.push({ id, role, content });
140
+ // html persists on the record: stopStreaming() and the conversation
141
+ // setter must never push an html message back through the markdown path.
142
+ this.#messages.push({ id, role, content, html });
128
143
 
129
144
  const el = document.createElement('div');
130
145
  el.setAttribute('data-role', role);
@@ -133,10 +148,13 @@ class ChatShell extends UIElement {
133
148
  if (role === 'user') {
134
149
  el.innerHTML = `<div data-bubble>${escapeHTML(content)}</div>`;
135
150
  } else if (role === 'assistant') {
136
- const rendered = renderMd && content ? renderMarkdown(content) : escapeHTML(content);
151
+ const done = renderMd || html;
152
+ const rendered = html ? content
153
+ : renderMd && content ? renderMarkdown(content)
154
+ : escapeHTML(content);
137
155
  el.innerHTML = `
138
156
  <span data-avatar>AI</span>
139
- <div data-bubble><div data-content>${rendered}</div>${!renderMd ? '<span data-cursor></span>' : ''}</div>
157
+ <div data-bubble><div data-content>${rendered}</div>${!done ? '<span data-cursor></span>' : ''}</div>
140
158
  `;
141
159
  } else if (role === 'error') {
142
160
  el.innerHTML = `<icon-ui name="warning"></icon-ui><span>${escapeHTML(content)}</span>`;
@@ -202,9 +220,11 @@ class ChatShell extends UIElement {
202
220
  // Remove cursor
203
221
  this.#messagesEl?.querySelector('[data-role]:last-child [data-cursor]')?.remove();
204
222
 
205
- // Render markdown in last assistant bubble
223
+ // Render markdown in last assistant bubble — never an html-mode
224
+ // message: its component markup is already live DOM, and the markdown
225
+ // path would re-escape it to source text.
206
226
  const last = this.#messages[this.#messages.length - 1];
207
- if (last?.role === 'assistant' && last.content) {
227
+ if (last?.role === 'assistant' && last.content && !last.html) {
208
228
  const contentEl = this.#messagesEl?.querySelector('[data-role]:last-child [data-content]');
209
229
  if (contentEl) {
210
230
  contentEl.innerHTML = renderMarkdown(last.content);
@@ -202,6 +202,8 @@ class ChatSidebar extends UIElement {
202
202
  // ── Child ResizeObserver — flips select-ui placement in narrow mode ──
203
203
 
204
204
  #setupChildResizeObserver() {
205
+ // gh#285 — SSR DOM shims (linkedom) have no ResizeObserver global.
206
+ if (typeof ResizeObserver === 'undefined') return;
205
207
  this.#childRO = new ResizeObserver((entries) => {
206
208
  for (const entry of entries) {
207
209
  const narrow = entry.contentBoxSize[0].inlineSize <= SNAP_THRESHOLD;
@@ -81,6 +81,8 @@ class ChatThread extends UIElement {
81
81
  }
82
82
 
83
83
  #setupChildObserver() {
84
+ // gh#285 — SSR DOM shims (linkedom) have no MutationObserver global.
85
+ if (typeof MutationObserver === 'undefined') return;
84
86
  this.#childObserver = new MutationObserver(() => {
85
87
  this.#syncEmptyFromChildren();
86
88
  // New message added — scroll to bottom if user is at bottom