@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 +5 -2
- package/package.json +1 -1
- package/traits/index.js +5 -0
- package/traits/scroll-lock.test.js +3 -1
- package/traits/traits-host.js +53 -0
- package/traits/traits-host.test.js +73 -0
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/ —
|
|
61
|
-
│
|
|
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
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
|
+
});
|