@adia-ai/web-modules 0.7.27 → 0.8.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.
@@ -207,6 +207,153 @@ describe('editor-sidebar', () => {
207
207
  globalThis.getComputedStyle = origGCS;
208
208
  });
209
209
 
210
+ // ── GH #223: expand() permanently wedges when the width transition is enabled ──
211
+ // Root cause: pane-ui's real CSS transition (editor-shell.bespoke.css) animates
212
+ // width from 48px toward the restore target; early ResizeObserver samples
213
+ // mid-transition are legitimately still <= SNAP_THRESHOLD, and the old
214
+ // #syncCollapsed() couldn't tell that apart from a genuine drag-to-collapse
215
+ // settle — flipping [collapsed] back to true mid-flight re-matches the
216
+ // `!important` collapsed-width CSS rule, which snaps the pane back to 48px
217
+ // and permanently interrupts the transition (no further resize ever occurs
218
+ // to self-correct). These tests mock a nonzero `transitionDuration` on the
219
+ // pane (jsdom/happy-dom has no real CSS loaded) to exercise the suppression.
220
+ describe('expand() transition race (GH #223)', () => {
221
+ function withPaneTransitionDuration(pane, durationStr) {
222
+ const origGCS = globalThis.getComputedStyle;
223
+ globalThis.getComputedStyle = (el, ps) => {
224
+ if (el === pane) return { ...origGCS(el, ps), transitionDuration: durationStr };
225
+ return origGCS(el, ps);
226
+ };
227
+ return () => { globalThis.getComputedStyle = origGCS; };
228
+ }
229
+
230
+ it('does not latch [collapsed] on a small mid-transition RO sample while an explicit expand() is in flight', async () => {
231
+ let roCallback;
232
+ const savedRO = globalThis.ResizeObserver;
233
+ globalThis.ResizeObserver = class {
234
+ constructor(cb) { roCallback = cb; }
235
+ observe() {} unobserve() {} disconnect() {}
236
+ };
237
+
238
+ const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
239
+ const pane = sb.querySelector('pane-ui');
240
+ const restoreGCS = withPaneTransitionDuration(pane, '0.18s');
241
+ pane.style.width = '48px';
242
+ sb.collapsed = true;
243
+
244
+ sb.expand(); // sets pane width to the restore target + marks expand-in-flight
245
+
246
+ // Mid-transition: the observer samples an intermediate, still-small width.
247
+ pane.style.width = '77px';
248
+ if (roCallback) roCallback([{ target: pane }]);
249
+ await new Promise((r) => requestAnimationFrame(r));
250
+ await tick();
251
+
252
+ // Must NOT have snapped back to collapsed — this is exactly the wedge bug.
253
+ expect(sb.collapsed).toBe(false);
254
+ expect(sb.hasAttribute('collapsed')).toBe(false);
255
+
256
+ restoreGCS();
257
+ globalThis.ResizeObserver = savedRO;
258
+ });
259
+
260
+ it('clears expand-in-flight and re-syncs on transitionend, landing on the correct final state', async () => {
261
+ let roCallback;
262
+ const savedRO = globalThis.ResizeObserver;
263
+ globalThis.ResizeObserver = class {
264
+ constructor(cb) { roCallback = cb; }
265
+ observe() {} unobserve() {} disconnect() {}
266
+ };
267
+
268
+ const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
269
+ const pane = sb.querySelector('pane-ui');
270
+ const restoreGCS = withPaneTransitionDuration(pane, '0.18s');
271
+ pane.style.width = '48px';
272
+ sb.collapsed = true;
273
+
274
+ sb.expand();
275
+ pane.style.width = '240px'; // transition has now settled at its target
276
+ // `propertyName` is a TransitionEvent field, not a plain Event
277
+ // constructor option (the constructor silently drops unknown keys) —
278
+ // assign it as an own property after construction instead.
279
+ const transitionEnd = new Event('transitionend', { bubbles: false });
280
+ transitionEnd.propertyName = 'width';
281
+ pane.dispatchEvent(transitionEnd);
282
+ await tick();
283
+
284
+ expect(sb.collapsed).toBe(false);
285
+ expect(sb.hasAttribute('collapsed')).toBe(false);
286
+
287
+ // With expand-in-flight cleared, a genuinely small sample (e.g. a
288
+ // later user drag-to-collapse) must resume being trusted normally.
289
+ pane.style.width = '48px';
290
+ if (roCallback) roCallback([{ target: pane }]);
291
+ await new Promise((r) => requestAnimationFrame(r));
292
+ await tick();
293
+ expect(sb.collapsed).toBe(true);
294
+
295
+ restoreGCS();
296
+ globalThis.ResizeObserver = savedRO;
297
+ });
298
+
299
+ it('falls back to a bounded timer sized to the computed duration when transitionend never fires', async () => {
300
+ vi.useFakeTimers();
301
+ let roCallback;
302
+ const savedRO = globalThis.ResizeObserver;
303
+ globalThis.ResizeObserver = class {
304
+ constructor(cb) { roCallback = cb; }
305
+ observe() {} unobserve() {} disconnect() {}
306
+ };
307
+
308
+ const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
309
+ const pane = sb.querySelector('pane-ui');
310
+ const restoreGCS = withPaneTransitionDuration(pane, '0.18s');
311
+ pane.style.width = '48px';
312
+ sb.collapsed = true;
313
+
314
+ sb.expand();
315
+ pane.style.width = '77px'; // stuck reporting a small value; no transitionend ever arrives
316
+ if (roCallback) roCallback([{ target: pane }]);
317
+ await vi.advanceTimersByTimeAsync(50); // rAF-equivalent settle window (fake timers)
318
+
319
+ // Still suppressed — the 180ms+50ms safety timer hasn't elapsed yet.
320
+ expect(sb.collapsed).toBe(false);
321
+
322
+ pane.style.width = '240px'; // by the time the safety timer fires, transition is done
323
+ await vi.advanceTimersByTimeAsync(300); // past the 180+50ms fallback
324
+ expect(sb.collapsed).toBe(false); // final re-sync sees the real (large) width
325
+
326
+ restoreGCS();
327
+ globalThis.ResizeObserver = savedRO;
328
+ vi.useRealTimers();
329
+ });
330
+
331
+ it('skips expand-in-flight suppression entirely when the computed transition duration is 0 (no transition to race)', async () => {
332
+ let roCallback;
333
+ const savedRO = globalThis.ResizeObserver;
334
+ globalThis.ResizeObserver = class {
335
+ constructor(cb) { roCallback = cb; }
336
+ observe() {} unobserve() {} disconnect() {}
337
+ };
338
+
339
+ const sb = mount('<editor-sidebar slot="leading" collapsible><pane-ui></pane-ui></editor-sidebar>');
340
+ const pane = sb.querySelector('pane-ui');
341
+ // No transitionDuration mock — jsdom/happy-dom defaults to '0s'.
342
+ pane.style.width = '48px';
343
+ sb.collapsed = true;
344
+
345
+ sb.expand();
346
+ // With a 0-duration transition, the width jump is instantaneous — the
347
+ // observer's first sample already lands at the final (large) width.
348
+ pane.style.width = '240px';
349
+ if (roCallback) roCallback([{ target: pane }]);
350
+ await tick();
351
+
352
+ expect(sb.collapsed).toBe(false);
353
+ globalThis.ResizeObserver = savedRO;
354
+ });
355
+ });
356
+
210
357
  it('handles missing inner <pane-ui> gracefully (no crash)', () => {
211
358
  const sb = mount('<editor-sidebar slot="leading"></editor-sidebar>');
212
359
  // Should not throw
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-modules",
3
- "version": "0.7.27",
3
+ "version": "0.8.0",
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": {
@@ -275,9 +275,9 @@
275
275
  "./theme/**/*.js"
276
276
  ],
277
277
  "peerDependencies": {
278
- "@adia-ai/web-components": "^0.7.0",
279
- "@adia-ai/a2ui-runtime": "^0.7.0",
280
- "@adia-ai/llm": "^0.7.0"
278
+ "@adia-ai/web-components": "^0.8.0",
279
+ "@adia-ai/a2ui-runtime": "^0.8.0",
280
+ "@adia-ai/llm": "^0.8.0"
281
281
  },
282
282
  "publishConfig": {
283
283
  "access": "public",
@@ -72,7 +72,15 @@ admin-sidebar > :is(admin-topbar, admin-statusbar) > :where(span, p, small, time
72
72
  padding-inline: var(--a-space-2);
73
73
  }
74
74
 
75
- /* ── admin-scroll ≡ <section> child of <main> ── */
75
+ /* ── admin-scroll ≡ <section> child of <main> ──
76
+ Full border, not border-inline (2026-07-14): with "rounded" mode's
77
+ 20px radius, an inline-only border TAPERS OUT halfway around each
78
+ corner arc (CSS paints the left border's corner segment only up to
79
+ the diagonal where the absent top border would take over) while the
80
+ box-shadow ring traces the full corner — two divergent lines at
81
+ exactly the radius, reading as a doubled border (operator-reported,
82
+ screenshot). A full 1px border draws one continuous arc the shadow
83
+ hugs, the standard bordered-card look. */
76
84
  admin-content > admin-scroll {
77
85
  flex: 1;
78
86
  min-height: 0;
@@ -80,7 +88,7 @@ admin-content > admin-scroll {
80
88
  overscroll-behavior: contain;
81
89
  scrollbar-width: none;
82
90
  background: var(--page-content-bg);
83
- border-inline: var(--page-content-border);
91
+ border: var(--page-content-border);
84
92
  box-shadow: var(--page-content-shadow);
85
93
  }
86
94
 
@@ -107,20 +115,24 @@ admin-shell[mode~="rounded"] > admin-content > admin-scroll {
107
115
  }
108
116
 
109
117
  /* ── admin-page-header ≡ <[data-content-header]> sticky band ──
110
- Background uses --page-content-header-bg (defaults to --a-canvas-1, one
111
- step raised from --page-content-bg / --a-canvas-0) so the sticky band
112
- visually distinguishes itself from the content surface scrolling under
113
- it. A subtle drop shadow below the band reinforces "chrome floating
114
- above content" on both light and dark themes the canvas-step alone
115
- reads too quietly in dark mode (ΔL 0.04 on the OKLCH scale). The
116
- border-bottom remains for crisp edge separation. */
118
+ Background uses --page-content-header-bg (defaults to --a-canvas-0,
119
+ same step as --page-content-bg v0.6.1x rebase, see
120
+ admin-shell.tokens.css's own history comment) so the sticky band reads
121
+ as a flush extension of the content surface, not separate chrome. No
122
+ box-shadow (removed was --a-shadow-sm, a leftover from when the
123
+ header used a raised --a-canvas-1 and needed its own drop shadow to
124
+ read as distinct chrome; once flush, that shadow doubled up against
125
+ admin-scroll's own container shadow at the rounded top corner —
126
+ visible as a doubled border in "rounded" mode). border-bottom alone
127
+ carries the separation now, same pattern as admin-page-footer below
128
+ (its own comment already states this — "reads correctly from the
129
+ border-top + canvas step alone"). */
117
130
  admin-page > admin-page-header {
118
131
  position: sticky;
119
132
  top: 0;
120
133
  z-index: 1;
121
134
  background: var(--page-content-header-bg);
122
135
  border-bottom: var(--page-content-border);
123
- box-shadow: var(--a-shadow-sm);
124
136
  flex-shrink: 0;
125
137
  }
126
138
 
@@ -242,7 +254,7 @@ admin-sidebar[collapsed] {
242
254
 
243
255
  :is(admin-topbar, admin-statusbar) > [slot="heading"] {
244
256
  font-weight: var(--a-weight-medium, 500);
245
- color: var(--a-fg);
257
+ color: var(--md-sys-color-neutral-on-surface);
246
258
  }
247
259
 
248
260
  :is(admin-topbar, admin-statusbar) > [slot="description"] {
@@ -76,7 +76,7 @@ admin-entity-item > [slot="label"] {
76
76
  white-space: nowrap;
77
77
  font-size: var(--a-ui-sm);
78
78
  font-weight: var(--a-weight-medium, 500);
79
- color: var(--a-fg);
79
+ color: var(--md-sys-color-neutral-on-surface);
80
80
  }
81
81
 
82
82
  /* Optional trailing badge — environment / role chip. Never shrinks. */
@@ -65,7 +65,7 @@ admin-shell > admin-content > admin-scroll > [data-subnav] {
65
65
  min-height: 0;
66
66
  overflow-y: auto;
67
67
  overscroll-behavior: contain;
68
- border-inline-end: 1px solid var(--a-border-subtle);
68
+ border-inline-end: 1px solid var(--md-sys-color-neutral-outline-variant);
69
69
  padding-block: var(--a-space-2);
70
70
  padding-inline: var(--a-space-2);
71
71
  }
@@ -77,6 +77,6 @@ admin-shell > admin-content > admin-scroll > [data-subnav] {
77
77
  }
78
78
  admin-shell > admin-content > admin-scroll > [data-subnav] {
79
79
  border-inline-end: none;
80
- border-block-end: 1px solid var(--a-border-subtle);
80
+ border-block-end: 1px solid var(--md-sys-color-neutral-outline-variant);
81
81
  }
82
82
  }
@@ -106,7 +106,7 @@ admin-sidebar[slot="trailing"] {
106
106
  :is(admin-sidebar[slot="leading"], admin-sidebar[slot="trailing"]) > :is(header, header-ui) > [slot="heading"],
107
107
  :is(admin-sidebar[slot="leading"], admin-sidebar[slot="trailing"]) > :is(footer, footer-ui) > [slot="heading"] {
108
108
  font-weight: var(--a-weight-medium);
109
- color: var(--a-fg);
109
+ color: var(--md-sys-color-neutral-on-surface);
110
110
  }
111
111
  :is(admin-sidebar[slot="leading"], admin-sidebar[slot="trailing"]) > :is(header, header-ui) > [slot="description"],
112
112
  :is(admin-sidebar[slot="leading"], admin-sidebar[slot="trailing"]) > :is(footer, footer-ui) > [slot="description"] {
@@ -14,8 +14,8 @@
14
14
 
15
15
  :where(admin-shell) {
16
16
  /* Shell — root backgrounds and border shorthand */
17
- --page-bg: var(--a-canvas-dim); /* shell chrome bg */
18
- --page-border: 1px solid var(--a-border-subtle); /* chrome borders (killed by "borderless") */
17
+ --page-bg: var(--md-sys-color-neutral-surface-dim); /* shell chrome bg */
18
+ --page-border: 1px solid var(--md-sys-color-neutral-outline-variant); /* chrome borders (killed by "borderless") */
19
19
  --page-main-border: var(--page-border); /* main inline borders (separate so modes can override) */
20
20
 
21
21
  /* Header / footer bars — shared by topbar + bottombar */
@@ -31,9 +31,9 @@
31
31
  --page-sidebar-font: var(--a-ui-size);
32
32
  --page-sidebar-min-width: 48px; /* collapsed icon-only width */
33
33
  --page-sidebar-max-width: 480px; /* drag resize upper bound */
34
- --page-sidebar-resize-accent: var(--a-accent); /* resize handle color */
35
- --page-sidebar-bg: var(--a-canvas-dimmest); /* sidebar surface — distinct from the chrome (--page-bg) */
36
- --page-sidebar-divider-bg: var(--a-border-subtle); /* hr divider inside sidebar */
34
+ --page-sidebar-resize-accent: var(--md-sys-color-primary); /* resize handle color */
35
+ --page-sidebar-bg: var(--md-sys-color-neutral-surface-dimmest); /* sidebar surface — distinct from the chrome (--page-bg) */
36
+ --page-sidebar-divider-bg: var(--md-sys-color-neutral-outline-variant); /* hr divider inside sidebar */
37
37
  --page-sidebar-width-leading: clamp(var(--page-sidebar-min-width), 200px, 240px);
38
38
  --page-sidebar-width-trailing: clamp(var(--page-sidebar-min-width), 200px, 240px);
39
39
 
@@ -43,7 +43,7 @@
43
43
  --page-content-radius: var(--a-radius-lg); /* used by "rounded" mode */
44
44
  --page-content-inset: var(--a-space-10); /* padding for header/body/footer */
45
45
  --page-content-max-width: 1540px; /* max-width for content children */
46
- --page-content-border: 1px solid var(--a-border-subtle); /* content dividers (persists in "borderless") */
46
+ --page-content-border: 1px solid var(--md-sys-color-neutral-outline-variant); /* content dividers (persists in "borderless") */
47
47
  --page-content-shadow: var(--a-shadow-sm); /* soft lift on the content surface */
48
48
 
49
49
  /* Content breakpoint scale — `<admin-page>` is a `page-content`
@@ -110,11 +110,11 @@
110
110
  --nav-label-font-size: var(--a-kicker-sm);
111
111
  --nav-label-weight: var(--a-weight-medium);
112
112
  --nav-label-fg: var(--a-fg-muted);
113
- --nav-divider-bg: var(--a-border-subtle);
113
+ --nav-divider-bg: var(--md-sys-color-neutral-outline-variant);
114
114
  --nav-divider-my: var(--a-space-1);
115
115
 
116
116
  /* Nav — group tokens */
117
- --nav-group-indent-rail-bg: var(--a-border-subtle);
117
+ --nav-group-indent-rail-bg: var(--md-sys-color-neutral-outline-variant);
118
118
  --nav-group-icon-font-size: var(--a-ui-size);
119
119
  --nav-group-text-weight: var(--a-weight-medium);
120
120
  --nav-group-badge-bg: var(--a-bg-muted);
@@ -130,7 +130,7 @@
130
130
  --nav-item-badge-radius: var(--a-radius-full);
131
131
  --nav-item-trailing-font: var(--a-ui-sm);
132
132
  --nav-item-trailing-fg: var(--a-fg-muted);
133
- --nav-item-trailing-border: var(--a-border-subtle);
133
+ --nav-item-trailing-border: var(--md-sys-color-neutral-outline-variant);
134
134
  --nav-item-trailing-radius: var(--a-radius-sm);
135
135
  --nav-item-trailing-px: var(--a-space-0-5);
136
136
 
@@ -11,9 +11,9 @@ embed-shell {
11
11
  position: relative; /* containing block for <760px cover-sheet panels */
12
12
  box-sizing: border-box;
13
13
  overflow: hidden; /* clip columns + cover-sheets to the rounded frame */
14
- background: var(--a-bg);
15
- color: var(--a-fg);
16
- border: 1px solid var(--a-border-subtle);
14
+ background: var(--md-sys-color-neutral-background);
15
+ color: var(--md-sys-color-neutral-on-surface);
16
+ border: 1px solid var(--md-sys-color-neutral-outline-variant);
17
17
  border-radius: var(--a-radius-lg, var(--a-radius-md));
18
18
  box-shadow: var(--a-shadow-lg);
19
19
  font-family: var(--a-font-family);
@@ -57,7 +57,7 @@ embed-shell > [panel] {
57
57
  flex: none;
58
58
  inline-size: 50%;
59
59
  block-size: 100%;
60
- border-inline-start: 1px solid var(--a-border-subtle);
60
+ border-inline-start: 1px solid var(--md-sys-color-neutral-outline-variant);
61
61
  }
62
62
  }
63
63
 
@@ -68,7 +68,7 @@ embed-shell > [panel] {
68
68
  position: absolute;
69
69
  inset: 0;
70
70
  z-index: 5;
71
- background: var(--a-bg);
71
+ background: var(--md-sys-color-neutral-background);
72
72
  transform: translateY(100%); /* parked below; identity at rest → popovers OK */
73
73
  transition: transform 260ms var(--a-easing-out, ease);
74
74
  }