@adia-ai/web-components 0.2.1 → 0.2.2

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/README.md CHANGED
@@ -57,8 +57,11 @@ web-components/
57
57
  │ gen-ui, a2ui-root) ship in the sibling `@adia-ai/web-modules`
58
58
  │ package as of 0.0.29 — see ADR-0012 for the three-tier rationale.
59
59
 
60
- ├── traits/ — 42 composable behaviors via defineTrait()
61
- (pressable, focusTrap, confetti, resizable, …)
60
+ ├── traits/ — 41 composable behaviors via defineTrait() + the
61
+ <traits-host> wrapper for raw-HTML declarative
62
+ │ composition. Generated catalog at _catalog.json
63
+ │ drives the MCP get_traits tool + per-trait demo
64
+ │ pages. Full contract in docs/specs/traits.md.
62
65
 
63
66
  ├── a2ui/ — deprecation shim for one release
64
67
  │ └── index.js Re-exports @adia-ai/a2ui-utils with a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-utils.",
5
5
  "type": "module",
6
6
  "exports": {
package/traits/index.js CHANGED
@@ -2,6 +2,11 @@
2
2
  // _catalog.json (generated from defineTrait() metadata). When adding a
3
3
  // trait, place it under its category header here, then run
4
4
  // `npm run build:traits` to refresh the catalog.
5
+ //
6
+ // Side-effect: the <traits-host> wrapper is auto-registered when this
7
+ // barrel is imported, so consumers who use the trait library at all
8
+ // also get raw-HTML declarative composition for free.
9
+ import './traits-host.js';
5
10
 
6
11
  // input-interaction
7
12
  export { pressable } from './pressable.js';
@@ -10,9 +10,11 @@ describe('scroll-lock', () => {
10
10
 
11
11
  it('connect sets body overflow:hidden + active attribute', () => {
12
12
  const host = mountHost();
13
- connectTrait(scrollLock, host);
13
+ const inst = connectTrait(scrollLock, host);
14
14
  expect(document.body.style.overflow).toBe('hidden');
15
15
  expect(host.hasAttribute('data-scroll-lock-active')).toBe(true);
16
+ // Balanced disconnect to keep the module-level lockCount honest for sibling tests.
17
+ inst.disconnect(host);
16
18
  });
17
19
 
18
20
  it('disconnect restores body overflow + clears attribute', () => {
@@ -0,0 +1,53 @@
1
+ /**
2
+ * <traits-host traits="pressable scale-press ripple">
3
+ * <div>raw markup with trait behaviors attached</div>
4
+ * </traits-host>
5
+ *
6
+ * Tiny pass-through wrapper that extends declarative trait composition
7
+ * to raw HTML. Without this element, only UIElement subclasses can read
8
+ * the `traits=` attribute. Wrap any markup in <traits-host> and the
9
+ * named traits attach to the wrapper itself — events bubble up from
10
+ * children, attribute toggles land on the wrapper, and the children
11
+ * render unaffected.
12
+ *
13
+ * The host uses `display: contents` so it does not introduce a layout
14
+ * box; the wrapped children participate in the parent's flex/grid
15
+ * exactly as if the wrapper were not there.
16
+ *
17
+ * Use this for:
18
+ * - sprinkling `pressable` onto a custom button you don't want to
19
+ * turn into a UIElement
20
+ * - giving a `<dialog>` or `<details>` a `focus-trap` without a
21
+ * wrapper component
22
+ * - composing `magnetic-hover` + `tilt-hover` onto an existing
23
+ * marketing CTA without rewriting it as a component
24
+ *
25
+ * For UIElement subclasses, prefer the bare attribute on the element
26
+ * itself: `<button-ui traits="ripple">…</button-ui>`. The wrapper is
27
+ * for cases where the host element is NOT a UIElement.
28
+ */
29
+
30
+ import { UIElement } from '../core/element.js';
31
+
32
+ class TraitsHost extends UIElement {
33
+ static template = () => null;
34
+
35
+ connected() {
36
+ // No-op — UIElement reads `traits` attribute and applies behavior.
37
+ }
38
+ }
39
+
40
+ if (typeof customElements !== 'undefined' && !customElements.get('traits-host')) {
41
+ customElements.define('traits-host', TraitsHost);
42
+ }
43
+
44
+ // One-shot stylesheet keeps the host out of the layout flow so children
45
+ // render in the parent's box.
46
+ if (typeof document !== 'undefined' && document.head && !document.querySelector('#adia-traits-host-style')) {
47
+ const style = document.createElement('style');
48
+ style.id = 'adia-traits-host-style';
49
+ style.textContent = 'traits-host { display: contents; }';
50
+ document.head.appendChild(style);
51
+ }
52
+
53
+ export { TraitsHost };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * <traits-host> behavior tests — focus on the wrapper-specific contract:
3
+ * - children pass through visually (display: contents)
4
+ * - declarative traits attach to the wrapper
5
+ * - traits attribute swaps work the same as on UIElement subclasses
6
+ * - events from children bubble up to the wrapper for trait capture
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach } from 'vitest';
10
+ import './traits-host.js';
11
+ import './pressable.js';
12
+ import './hoverable.js';
13
+ import { resetDOM } from './_test-helpers.js';
14
+
15
+ describe('<traits-host>', () => {
16
+ beforeEach(resetDOM);
17
+
18
+ it('is registered as a UIElement subclass', () => {
19
+ const ctor = customElements.get('traits-host');
20
+ expect(ctor).toBeTruthy();
21
+ });
22
+
23
+ it('declarative traits=" " attaches to the wrapper itself', () => {
24
+ document.body.innerHTML = '<traits-host traits="pressable"><div>Raw</div></traits-host>';
25
+ const host = document.body.firstElementChild;
26
+ host.dispatchEvent(new PointerEvent('pointerdown'));
27
+ expect(host.hasAttribute('data-pressable-pressed')).toBe(true);
28
+ host.dispatchEvent(new PointerEvent('pointerup'));
29
+ expect(host.hasAttribute('data-pressable-pressed')).toBe(false);
30
+ });
31
+
32
+ it('events from children bubble up so the trait captures them', () => {
33
+ document.body.innerHTML = '<traits-host traits="hoverable"><span class="inner">Hover</span></traits-host>';
34
+ const host = document.body.firstElementChild;
35
+ const inner = host.querySelector('.inner');
36
+ // Synthesize a pointerenter on the wrapper (DOM bubbling up from inner
37
+ // would also work but happy-dom doesn't always reflect that for hover).
38
+ host.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true }));
39
+ expect(host.hasAttribute('data-hoverable-hover')).toBe(true);
40
+ host.dispatchEvent(new PointerEvent('pointerleave', { bubbles: true }));
41
+ expect(host.hasAttribute('data-hoverable-hover')).toBe(false);
42
+ });
43
+
44
+ it('changing traits attribute swaps the trait stack', () => {
45
+ document.body.innerHTML = '<traits-host traits="pressable"><div>x</div></traits-host>';
46
+ const host = document.body.firstElementChild;
47
+ host.dispatchEvent(new PointerEvent('pointerdown'));
48
+ expect(host.hasAttribute('data-pressable-pressed')).toBe(true);
49
+ host.dispatchEvent(new PointerEvent('pointerup'));
50
+
51
+ host.setAttribute('traits', 'hoverable');
52
+ host.dispatchEvent(new PointerEvent('pointerdown'));
53
+ expect(host.hasAttribute('data-pressable-pressed')).toBe(false);
54
+ host.dispatchEvent(new PointerEvent('pointerenter'));
55
+ expect(host.hasAttribute('data-hoverable-hover')).toBe(true);
56
+ });
57
+
58
+ it('removing the host cleans up trait attributes', () => {
59
+ document.body.innerHTML = '<traits-host traits="pressable"><div>x</div></traits-host>';
60
+ const host = document.body.firstElementChild;
61
+ host.dispatchEvent(new PointerEvent('pointerdown'));
62
+ expect(host.hasAttribute('data-pressable-pressed')).toBe(true);
63
+ host.remove();
64
+ expect(host.hasAttribute('data-pressable-pressed')).toBe(false);
65
+ });
66
+
67
+ it('with no traits attribute: pure pass-through, children render', () => {
68
+ document.body.innerHTML = '<traits-host><span class="x">child</span></traits-host>';
69
+ const host = document.body.firstElementChild;
70
+ expect(host.querySelector('.x')).toBeTruthy();
71
+ expect(host.querySelector('.x').textContent).toBe('child');
72
+ });
73
+ });