@adia-ai/web-modules 0.6.27 → 0.6.28

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,11 @@
1
1
  # Changelog — @adia-ai/web-modules
2
2
 
3
+ ## [0.6.28] — 2026-05-23
4
+
5
+ ### Fixed — `editor-sidebar` rAF debounce for wide→narrow display-flip transient (§FB-50)
6
+
7
+ - **`editor-sidebar` — wide→narrow viewport transition latches `[collapsed]` even after v0.6.27's `getComputedStyle` guard.** The v0.6.27 guard only protects the narrow→wide direction. During wide→narrow, the CSS Grid track collapses the pane to 48px before `display:none` propagates to `getComputedStyle`; the guard sees `display:flex` and runs `#syncCollapsed()` against the transient 48px sample, latching `collapsed=true`. Fix: `#syncCollapsed()` now defers `collapsed=true` writes by one `requestAnimationFrame`. Expand signals (`rect.width > SNAP_THRESHOLD`) are applied synchronously; collapse signals wait one frame — by which point `display:none` has propagated and the `getComputedStyle` guard catches it. Pending rAF is cancelled on expand signals and on `disconnected()`. Intentional drag-to-collapse fires correctly after the drag settles. Added regression tests: (1) small-width RO sample does not immediately latch `collapsed`; (2) `display:none` before the rAF prevents the latch. Reported by color-app (FEEDBACK-50). Files: `editor/editor-sidebar/editor-sidebar.js`, `editor/editor-sidebar/editor-sidebar.test.js`.
8
+
3
9
  ## [0.6.27] — 2026-05-22
4
10
 
5
11
  ### Fixed — `editor-sidebar` computedStyle guard for small-nonzero display-flip transients (§FB-49)
@@ -67,6 +67,7 @@ class EditorSidebar extends UIElement {
67
67
  #storageKey = null;
68
68
  #onPointerDown = null;
69
69
  #onPointerUp = null;
70
+ #pendingCollapseRaf = null;
70
71
 
71
72
  connected() {
72
73
  this.#pane = this.querySelector('pane-ui');
@@ -128,6 +129,10 @@ class EditorSidebar extends UIElement {
128
129
  }
129
130
 
130
131
  disconnected() {
132
+ if (this.#pendingCollapseRaf !== null) {
133
+ cancelAnimationFrame(this.#pendingCollapseRaf);
134
+ this.#pendingCollapseRaf = null;
135
+ }
131
136
  this.#ro?.disconnect();
132
137
  this.#ro = null;
133
138
  if (this.#onPointerDown && this.#pane) {
@@ -174,14 +179,37 @@ class EditorSidebar extends UIElement {
174
179
 
175
180
  #syncCollapsed() {
176
181
  if (!this.#pane) return;
177
- // §FB-49: guard before reading rect — catches explicit display:none AND
178
- // small-nonzero transients during display-flip (e.g. pane briefly at 48px
179
- // while re-entering layout). getComputedStyle resolves CSS rules, not just
180
- // inline style, covering both direct assignment and @media rules.
181
182
  if (getComputedStyle(this).display === 'none') return;
182
183
  const rect = this.#pane.getBoundingClientRect();
183
- if (rect.width === 0) return; // §FB-42 belt-and-braces for detached/hidden
184
- this.collapsed = rect.width <= SNAP_THRESHOLD;
184
+ if (rect.width === 0) return; // §FB-42 belt-and-braces
185
+
186
+ if (rect.width > SNAP_THRESHOLD) {
187
+ // Expand: apply immediately and cancel any pending collapse.
188
+ if (this.#pendingCollapseRaf !== null) {
189
+ cancelAnimationFrame(this.#pendingCollapseRaf);
190
+ this.#pendingCollapseRaf = null;
191
+ }
192
+ this.collapsed = false;
193
+ return;
194
+ }
195
+
196
+ // §FB-50: small sample — may be a CSS-grid collapse transient during
197
+ // the wide→narrow display-flip (pane shrinks before display:none
198
+ // propagates to getComputedStyle). Defer one animation frame; if
199
+ // display:none takes effect before the rAF fires the guard above
200
+ // catches it. Intentional drag-to-collapse also goes through this
201
+ // path and fires after the drag settles, which is correct.
202
+ if (this.#pendingCollapseRaf === null) {
203
+ this.#pendingCollapseRaf = requestAnimationFrame(() => {
204
+ this.#pendingCollapseRaf = null;
205
+ if (!this.#pane) return;
206
+ if (getComputedStyle(this).display === 'none') return;
207
+ const r = this.#pane.getBoundingClientRect();
208
+ if (r.width > 0 && r.width <= SNAP_THRESHOLD) {
209
+ this.collapsed = true;
210
+ }
211
+ });
212
+ }
185
213
  }
186
214
 
187
215
  #emitToggle() {
@@ -148,6 +148,65 @@ describe('editor-sidebar', () => {
148
148
  globalThis.getComputedStyle = origGCS;
149
149
  });
150
150
 
151
+ it('defers [collapsed]=true by one rAF for small-transient samples (§FB-50)', async () => {
152
+ let roCallback;
153
+ const savedRO = globalThis.ResizeObserver;
154
+ globalThis.ResizeObserver = class {
155
+ constructor(cb) { roCallback = cb; }
156
+ observe() {} unobserve() {} disconnect() {}
157
+ };
158
+
159
+ const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
160
+ const pane = sb.querySelector('pane-ui');
161
+ // Simulate a small width on the pane (CSS grid collapse transient or drag).
162
+ pane.style.width = '48px';
163
+
164
+ if (roCallback) roCallback([{ target: pane }]);
165
+ // Immediately after RO callback — collapsed must NOT be set yet (rAF pending).
166
+ expect(sb.collapsed).toBe(false);
167
+
168
+ // After the rAF fires — pane still 48px, host visible → collapse is applied.
169
+ await new Promise((r) => requestAnimationFrame(r));
170
+ await tick();
171
+ expect(sb.collapsed).toBe(true);
172
+
173
+ globalThis.ResizeObserver = savedRO;
174
+ });
175
+
176
+ it('rAF check skips collapse if display:none applied before frame fires (§FB-50 wide→narrow race)', async () => {
177
+ let roCallback;
178
+ const savedRO = globalThis.ResizeObserver;
179
+ globalThis.ResizeObserver = class {
180
+ constructor(cb) { roCallback = cb; }
181
+ observe() {} unobserve() {} disconnect() {}
182
+ };
183
+
184
+ const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
185
+ const pane = sb.querySelector('pane-ui');
186
+ // Transient 48px — CSS grid collapsed the track before display:none applied.
187
+ pane.style.width = '48px';
188
+
189
+ if (roCallback) roCallback([{ target: pane }]);
190
+ expect(sb.collapsed).toBe(false); // deferred, not set yet
191
+
192
+ // Simulate display:none propagating before the rAF executes (the race window).
193
+ sb.style.display = 'none';
194
+ const origGCS = globalThis.getComputedStyle;
195
+ globalThis.getComputedStyle = (el, ps) => {
196
+ if (el === sb && el.style.display === 'none') return { display: 'none' };
197
+ return origGCS(el, ps);
198
+ };
199
+
200
+ // rAF fires — guard sees display:none → collapsed stays false.
201
+ await new Promise((r) => requestAnimationFrame(r));
202
+ await tick();
203
+ expect(sb.collapsed).toBe(false);
204
+ expect(sb.hasAttribute('collapsed')).toBe(false);
205
+
206
+ globalThis.ResizeObserver = savedRO;
207
+ globalThis.getComputedStyle = origGCS;
208
+ });
209
+
151
210
  it('handles missing inner <pane-ui> gracefully (no crash)', () => {
152
211
  const sb = mount('<editor-sidebar slot="leading"></editor-sidebar>');
153
212
  // Should not throw
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-modules",
3
- "version": "0.6.27",
3
+ "version": "0.6.28",
4
4
  "description": "AdiaUI composite custom elements \u2014 shell, chat, editor, runtime clusters built from @adia-ai/web-components primitives. Subpath exports per cluster.",
5
5
  "type": "module",
6
6
  "exports": {