@adia-ai/web-modules 0.6.26 → 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,17 @@
|
|
|
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
|
+
|
|
9
|
+
## [0.6.27] — 2026-05-22
|
|
10
|
+
|
|
11
|
+
### Fixed — `editor-sidebar` computedStyle guard for small-nonzero display-flip transients (§FB-49)
|
|
12
|
+
|
|
13
|
+
- **`editor-sidebar` — ResizeObserver fires with small-nonzero widths during `display:none` → `display:flex` transitions, latching `[collapsed]` even after the §FB-42 / §FB-48 `rect.width === 0` guard shipped in v0.6.26.** The 0-width guard only caught exact-zero samples; the `display-flip` cycle produces a brief 48px sample (matching the upstream `editor-shell[collapsed] > pane-ui { width: 48px !important }` rail minimum) before the pane fully re-enters layout. `#syncCollapsed()` read the transient, set `collapsed = true`, and the mirror-write `this.style.width = '48px'` pinned the host's grid track. Fix: guard on `getComputedStyle(this).display === 'none'` at the top of both the ResizeObserver callback and `#syncCollapsed()` — resolves the full CSS cascade (not just inline `style.display`) so any `@media`-driven hide triggers the guard. The §FB-42 `rect.width === 0` guard is retained as belt-and-braces. Added regression test: RO fires with a 48px transient while `style.display === 'none'` → `collapsed` stays false. Consumer workaround (CSS counter-rule at `@media (min-width: 900px)`) can now be safely retired. Reported by color-app (FEEDBACK-49). Files: `editor/editor-sidebar/editor-sidebar.js`, `editor/editor-sidebar/editor-sidebar.test.js`.
|
|
14
|
+
|
|
3
15
|
## [0.6.26] — 2026-05-22
|
|
4
16
|
|
|
5
17
|
### Fixed — `editor-sidebar` actually applies the FB-42 `#syncCollapsed` 0-width guard (FB-48)
|
|
@@ -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');
|
|
@@ -92,6 +93,10 @@ class EditorSidebar extends UIElement {
|
|
|
92
93
|
// explicit width — the pane then overflows out of the sidebar and
|
|
93
94
|
// visually overlaps the canvas.
|
|
94
95
|
this.#ro = new ResizeObserver((entries) => {
|
|
96
|
+
// §FB-49: skip samples while the host is not in layout — catches both
|
|
97
|
+
// explicit display:none and small-nonzero transients during the
|
|
98
|
+
// display-flip transition (e.g. 48px when pane re-enters layout).
|
|
99
|
+
if (getComputedStyle(this).display === 'none') return;
|
|
95
100
|
this.#syncCollapsed();
|
|
96
101
|
// Use offsetWidth so the sidebar wraps the full pane border-box
|
|
97
102
|
// (contentRect would be 1px short on side="leading|trailing").
|
|
@@ -124,6 +129,10 @@ class EditorSidebar extends UIElement {
|
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
disconnected() {
|
|
132
|
+
if (this.#pendingCollapseRaf !== null) {
|
|
133
|
+
cancelAnimationFrame(this.#pendingCollapseRaf);
|
|
134
|
+
this.#pendingCollapseRaf = null;
|
|
135
|
+
}
|
|
127
136
|
this.#ro?.disconnect();
|
|
128
137
|
this.#ro = null;
|
|
129
138
|
if (this.#onPointerDown && this.#pane) {
|
|
@@ -170,11 +179,37 @@ class EditorSidebar extends UIElement {
|
|
|
170
179
|
|
|
171
180
|
#syncCollapsed() {
|
|
172
181
|
if (!this.#pane) return;
|
|
182
|
+
if (getComputedStyle(this).display === 'none') return;
|
|
173
183
|
const rect = this.#pane.getBoundingClientRect();
|
|
174
|
-
// §FB-42
|
|
175
|
-
|
|
176
|
-
if (rect.width
|
|
177
|
-
|
|
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
|
+
}
|
|
178
213
|
}
|
|
179
214
|
|
|
180
215
|
#emitToggle() {
|
|
@@ -116,6 +116,97 @@ describe('editor-sidebar', () => {
|
|
|
116
116
|
expect(removedTypes).toContain('pointerup');
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
it('does not latch [collapsed] on small-transient RO sample while host is display:none (§FB-49)', async () => {
|
|
120
|
+
// Capture the RO callback so we can fire it manually with a transient width.
|
|
121
|
+
let roCallback;
|
|
122
|
+
const savedRO = globalThis.ResizeObserver;
|
|
123
|
+
globalThis.ResizeObserver = class {
|
|
124
|
+
constructor(cb) { roCallback = cb; }
|
|
125
|
+
observe() {} unobserve() {} disconnect() {}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
|
|
129
|
+
const pane = sb.querySelector('pane-ui');
|
|
130
|
+
pane.style.width = '240px';
|
|
131
|
+
|
|
132
|
+
// Simulate display:none via inline style; stub getComputedStyle to reflect it.
|
|
133
|
+
sb.style.display = 'none';
|
|
134
|
+
const origGCS = globalThis.getComputedStyle;
|
|
135
|
+
globalThis.getComputedStyle = (el, ps) => {
|
|
136
|
+
if (el === sb && el.style.display === 'none') return { display: 'none' };
|
|
137
|
+
return origGCS(el, ps);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Fire the RO with a small-nonzero transient (48px = SNAP_THRESHOLD).
|
|
141
|
+
if (roCallback) roCallback([{ target: pane, contentRect: { width: 48 } }]);
|
|
142
|
+
await tick();
|
|
143
|
+
|
|
144
|
+
expect(sb.collapsed).toBe(false);
|
|
145
|
+
expect(sb.hasAttribute('collapsed')).toBe(false);
|
|
146
|
+
|
|
147
|
+
globalThis.ResizeObserver = savedRO;
|
|
148
|
+
globalThis.getComputedStyle = origGCS;
|
|
149
|
+
});
|
|
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
|
+
|
|
119
210
|
it('handles missing inner <pane-ui> gracefully (no crash)', () => {
|
|
120
211
|
const sb = mount('<editor-sidebar slot="leading"></editor-sidebar>');
|
|
121
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.
|
|
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": {
|