@adia-ai/web-components 0.2.2 → 0.2.4

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.
Files changed (121) hide show
  1. package/components/agent-trace/agent-trace.css +24 -3
  2. package/components/button/button.js +3 -0
  3. package/components/demo-toggle/demo-toggle.a2ui.json +144 -0
  4. package/components/demo-toggle/demo-toggle.css +120 -0
  5. package/components/demo-toggle/demo-toggle.js +144 -0
  6. package/components/demo-toggle/demo-toggle.test.js +102 -0
  7. package/components/demo-toggle/demo-toggle.yaml +144 -0
  8. package/components/index.js +1 -0
  9. package/components/input/input.js +11 -0
  10. package/components/list/list.css +66 -3
  11. package/components/nav-group/nav-group.a2ui.json +1 -1
  12. package/components/nav-group/nav-group.css +5 -5
  13. package/components/nav-group/nav-group.yaml +1 -1
  14. package/components/nav-item/nav-item.a2ui.json +1 -1
  15. package/components/nav-item/nav-item.css +3 -4
  16. package/components/nav-item/nav-item.yaml +1 -1
  17. package/components/textarea/textarea.js +10 -0
  18. package/core/icons.js +13 -1
  19. package/package.json +1 -1
  20. package/styles/components.css +1 -0
  21. package/styles/typography.css +1 -1
  22. package/traits/_catalog.json +258 -5
  23. package/traits/active-state.test.js +1 -1
  24. package/traits/anchor-positioning.js +205 -52
  25. package/traits/anchor-positioning.test.js +77 -4
  26. package/traits/announcer-stage.js +157 -0
  27. package/traits/announcer.js +145 -0
  28. package/traits/announcer.test.js +268 -0
  29. package/traits/arrow-grid-nav.js +234 -0
  30. package/traits/arrow-grid-nav.test.js +375 -0
  31. package/traits/attention-pulse.js +1 -1
  32. package/traits/attention-pulse.test.js +1 -1
  33. package/traits/confetti-burst.js +90 -60
  34. package/traits/confetti-burst.test.js +16 -8
  35. package/traits/confetti-stage.js +143 -0
  36. package/traits/confetti.js +44 -47
  37. package/traits/confetti.test.js +24 -5
  38. package/traits/count-up.js +31 -6
  39. package/traits/count-up.test.js +1 -1
  40. package/traits/declarative.test.js +1 -1
  41. package/traits/dirty-state.test.js +1 -1
  42. package/traits/drag-ghost.js +55 -3
  43. package/traits/drag-ghost.test.js +1 -1
  44. package/traits/draggable-list-item.js +279 -0
  45. package/traits/draggable-list-item.test.js +51 -0
  46. package/traits/draggable.js +14 -4
  47. package/traits/draggable.test.js +1 -1
  48. package/traits/drop-target.js +223 -0
  49. package/traits/drop-target.test.js +241 -0
  50. package/traits/droppable-collection.js +89 -0
  51. package/traits/droppable-collection.test.js +99 -0
  52. package/traits/droppable.js +125 -0
  53. package/traits/droppable.test.js +54 -0
  54. package/traits/error-shake.js +157 -0
  55. package/traits/error-shake.test.js +114 -0
  56. package/traits/fade-presence.test.js +1 -1
  57. package/traits/focus-restore.js +135 -0
  58. package/traits/focus-restore.test.js +202 -0
  59. package/traits/focus-trap.test.js +1 -1
  60. package/traits/focusable.test.js +1 -1
  61. package/traits/glow-focus.js +1 -1
  62. package/traits/glow-focus.test.js +1 -1
  63. package/traits/gradient-shift.js +1 -1
  64. package/traits/gradient-shift.test.js +1 -1
  65. package/traits/haptic-feedback.test.js +1 -1
  66. package/traits/hotkey.test.js +1 -1
  67. package/traits/hoverable.test.js +1 -1
  68. package/traits/index.js +15 -0
  69. package/traits/inertia-drag.js +9 -0
  70. package/traits/inertia-drag.test.js +1 -1
  71. package/traits/input-mask.js +328 -0
  72. package/traits/input-mask.test.js +151 -0
  73. package/traits/intersection-observer.test.js +1 -1
  74. package/traits/keyboard-nav.test.js +1 -1
  75. package/traits/keyboard-reorderable.js +254 -0
  76. package/traits/keyboard-reorderable.test.js +45 -0
  77. package/traits/layout-animation.js +229 -0
  78. package/traits/layout-animation.test.js +114 -0
  79. package/traits/long-press.js +212 -0
  80. package/traits/long-press.test.js +244 -0
  81. package/traits/magnetic-hover.js +1 -1
  82. package/traits/magnetic-hover.test.js +1 -1
  83. package/traits/noise-texture.js +7 -3
  84. package/traits/noise-texture.test.js +1 -1
  85. package/traits/parallax.js +1 -1
  86. package/traits/parallax.test.js +1 -1
  87. package/traits/portal.test.js +1 -1
  88. package/traits/pressable.test.js +1 -1
  89. package/traits/resettable.js +29 -3
  90. package/traits/resettable.test.js +34 -1
  91. package/traits/resizable.test.js +1 -1
  92. package/traits/resize-observer.test.js +1 -1
  93. package/traits/ripple.js +1 -1
  94. package/traits/ripple.test.js +1 -1
  95. package/traits/roving-tabindex.test.js +1 -1
  96. package/traits/scale-press.test.js +1 -1
  97. package/traits/scroll-lock.test.js +1 -1
  98. package/traits/scroll-progress.js +201 -0
  99. package/traits/scroll-progress.test.js +182 -0
  100. package/traits/shimmer-loading.js +1 -1
  101. package/traits/shimmer-loading.test.js +1 -1
  102. package/traits/{_smoke.test.js → smoke.test.js} +1 -1
  103. package/traits/snap-to-grid.test.js +1 -1
  104. package/traits/sound-feedback.test.js +1 -1
  105. package/traits/spring-animate.js +8 -3
  106. package/traits/spring-animate.test.js +1 -1
  107. package/traits/success-checkmark.js +222 -0
  108. package/traits/success-checkmark.test.js +120 -0
  109. package/traits/tilt-hover.js +1 -1
  110. package/traits/tilt-hover.test.js +1 -1
  111. package/traits/tossable.js +9 -0
  112. package/traits/tossable.test.js +1 -1
  113. package/traits/traits-host.test.js +1 -1
  114. package/traits/typeahead.test.js +1 -1
  115. package/traits/typewriter.js +1 -1
  116. package/traits/typewriter.test.js +1 -1
  117. package/traits/validation.test.js +1 -1
  118. package/traits/view-transition.js +140 -0
  119. package/traits/view-transition.test.js +268 -0
  120. /package/traits/{_motion.js → motion.js} +0 -0
  121. /package/traits/{_test-helpers.js → test-helpers.js} +0 -0
@@ -0,0 +1,212 @@
1
+ import { defineTrait } from './define.js';
2
+
3
+ export const longPress = defineTrait({
4
+ name: 'long-press',
5
+ category: 'input-interaction',
6
+ description: 'Press-and-hold trigger: fires after configurable duration with progress events',
7
+ attributes: [
8
+ 'data-long-press-active',
9
+ 'data-long-press-progress',
10
+ 'data-long-press-fired',
11
+ ],
12
+ events: ['long-press', 'long-press-cancelled', 'long-press-progress'],
13
+ config: [
14
+ 'data-long-press-duration',
15
+ 'data-long-press-tolerance',
16
+ 'data-long-press-progress-interval',
17
+ ],
18
+ setup({ host }) {
19
+ let active = false;
20
+ let fired = false;
21
+ let startX = 0;
22
+ let startY = 0;
23
+ let startedAt = 0;
24
+ let timerId = null;
25
+ let intervalId = null;
26
+ let suppressClick = false;
27
+
28
+ function readDuration() {
29
+ const v = parseInt(host.getAttribute('data-long-press-duration'), 10);
30
+ return Number.isFinite(v) && v > 0 ? v : 600;
31
+ }
32
+
33
+ function readTolerance() {
34
+ const v = parseInt(host.getAttribute('data-long-press-tolerance'), 10);
35
+ return Number.isFinite(v) && v >= 0 ? v : 8;
36
+ }
37
+
38
+ function readProgressInterval() {
39
+ const v = parseInt(host.getAttribute('data-long-press-progress-interval'), 10);
40
+ return Number.isFinite(v) && v > 0 ? v : 50;
41
+ }
42
+
43
+ function isDisabled() {
44
+ return host.hasAttribute('disabled');
45
+ }
46
+
47
+ function clearTimers() {
48
+ if (timerId !== null) {
49
+ clearTimeout(timerId);
50
+ timerId = null;
51
+ }
52
+ if (intervalId !== null) {
53
+ clearInterval(intervalId);
54
+ intervalId = null;
55
+ }
56
+ }
57
+
58
+ function reset() {
59
+ active = false;
60
+ clearTimers();
61
+ host.removeAttribute('data-long-press-active');
62
+ host.removeAttribute('data-long-press-progress');
63
+ host.removeAttribute('data-long-press-fired');
64
+ }
65
+
66
+ function cancel(reason) {
67
+ if (!active) return;
68
+ const wasFired = fired;
69
+ reset();
70
+ fired = false;
71
+ // Only the truly-cancelled press emits the cancelled event. A press
72
+ // that already committed (long-press fired) is a successful close,
73
+ // not a cancellation, even though the pointerup handler tears down
74
+ // the same state.
75
+ if (!wasFired) {
76
+ host.dispatchEvent(new CustomEvent('long-press-cancelled', {
77
+ bubbles: true,
78
+ detail: { reason },
79
+ }));
80
+ }
81
+ }
82
+
83
+ function commit() {
84
+ if (!active || fired) return;
85
+ fired = true;
86
+ // Final progress = 1 so consumers reading the attribute see the
87
+ // completed state before the fired flag flips.
88
+ host.setAttribute('data-long-press-progress', '1');
89
+ host.setAttribute('data-long-press-fired', '');
90
+ // Suppress the next click so consumers don't double-fire on the
91
+ // pointerup that completes the press.
92
+ suppressClick = true;
93
+ host.dispatchEvent(new CustomEvent('long-press', {
94
+ bubbles: true,
95
+ detail: { duration: performance.now() - startedAt },
96
+ }));
97
+ // Clear interval — duration timer already fired itself.
98
+ if (intervalId !== null) {
99
+ clearInterval(intervalId);
100
+ intervalId = null;
101
+ }
102
+ }
103
+
104
+ function onPointerDown(e) {
105
+ if (isDisabled()) return;
106
+ if (active) return;
107
+ active = true;
108
+ fired = false;
109
+ startX = e.clientX ?? 0;
110
+ startY = e.clientY ?? 0;
111
+ startedAt = performance.now();
112
+ const duration = readDuration();
113
+ const interval = readProgressInterval();
114
+
115
+ host.setAttribute('data-long-press-active', '');
116
+ host.setAttribute('data-long-press-progress', '0');
117
+
118
+ timerId = setTimeout(commit, duration);
119
+ intervalId = setInterval(() => {
120
+ if (!active || fired) return;
121
+ const elapsed = performance.now() - startedAt;
122
+ const progress = Math.min(elapsed / duration, 1);
123
+ host.setAttribute('data-long-press-progress', String(progress));
124
+ host.dispatchEvent(new CustomEvent('long-press-progress', {
125
+ bubbles: true,
126
+ detail: { progress },
127
+ }));
128
+ }, interval);
129
+ }
130
+
131
+ function onPointerUp() {
132
+ if (!active) return;
133
+ if (fired) {
134
+ // Successful long-press just completed — reset state silently
135
+ // without firing cancelled.
136
+ reset();
137
+ fired = false;
138
+ return;
139
+ }
140
+ cancel('release');
141
+ }
142
+
143
+ function onPointerLeave() {
144
+ if (!active) return;
145
+ if (fired) {
146
+ // Already committed; pointerleave during the post-fire frame is
147
+ // benign — clean up without dispatching cancelled.
148
+ reset();
149
+ fired = false;
150
+ return;
151
+ }
152
+ cancel('leave');
153
+ }
154
+
155
+ function onPointerMove(e) {
156
+ if (!active || fired) return;
157
+ const tolerance = readTolerance();
158
+ const dx = (e.clientX ?? 0) - startX;
159
+ const dy = (e.clientY ?? 0) - startY;
160
+ if (Math.hypot(dx, dy) > tolerance) {
161
+ cancel('move');
162
+ }
163
+ }
164
+
165
+ function onPointerCancel() {
166
+ if (!active) return;
167
+ cancel('pointer-cancel');
168
+ }
169
+
170
+ function onClick(e) {
171
+ // The click that follows a successful long-press is suppressed so
172
+ // consumers wired to both `long-press` and `click` don't double-fire.
173
+ if (suppressClick) {
174
+ suppressClick = false;
175
+ e.stopImmediatePropagation();
176
+ e.preventDefault();
177
+ }
178
+ }
179
+
180
+ function onContextMenu(e) {
181
+ // While a long-press is in flight (typical on touch), suppress the
182
+ // browser's native context menu so the trait's commit lands cleanly.
183
+ if (active) {
184
+ e.preventDefault();
185
+ }
186
+ }
187
+
188
+ host.addEventListener('pointerdown', onPointerDown);
189
+ host.addEventListener('pointerup', onPointerUp);
190
+ host.addEventListener('pointerleave', onPointerLeave);
191
+ host.addEventListener('pointermove', onPointerMove);
192
+ host.addEventListener('pointercancel', onPointerCancel);
193
+ // `click` runs in capture so we beat any consumer-attached click
194
+ // listener and stop the event before it propagates.
195
+ host.addEventListener('click', onClick, true);
196
+ host.addEventListener('contextmenu', onContextMenu);
197
+
198
+ return () => {
199
+ host.removeEventListener('pointerdown', onPointerDown);
200
+ host.removeEventListener('pointerup', onPointerUp);
201
+ host.removeEventListener('pointerleave', onPointerLeave);
202
+ host.removeEventListener('pointermove', onPointerMove);
203
+ host.removeEventListener('pointercancel', onPointerCancel);
204
+ host.removeEventListener('click', onClick, true);
205
+ host.removeEventListener('contextmenu', onContextMenu);
206
+ clearTimers();
207
+ host.removeAttribute('data-long-press-active');
208
+ host.removeAttribute('data-long-press-progress');
209
+ host.removeAttribute('data-long-press-fired');
210
+ };
211
+ },
212
+ });
@@ -0,0 +1,244 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { longPress } from './long-press.js';
3
+ import { mountHost, connectTrait, spyEvent, resetDOM, wait } from './test-helpers.js';
4
+
5
+ function pointerDown(host, x = 0, y = 0) {
6
+ host.dispatchEvent(new PointerEvent('pointerdown', { clientX: x, clientY: y, bubbles: true }));
7
+ }
8
+ function pointerUp(host, x = 0, y = 0) {
9
+ host.dispatchEvent(new PointerEvent('pointerup', { clientX: x, clientY: y, bubbles: true }));
10
+ }
11
+ function pointerMove(host, x, y) {
12
+ host.dispatchEvent(new PointerEvent('pointermove', { clientX: x, clientY: y, bubbles: true }));
13
+ }
14
+ function pointerLeave(host) {
15
+ host.dispatchEvent(new PointerEvent('pointerleave', { bubbles: true }));
16
+ }
17
+
18
+ describe('long-press', () => {
19
+ beforeEach(() => {
20
+ resetDOM();
21
+ vi.useFakeTimers();
22
+ });
23
+
24
+ it('sets data-long-press-active on pointerdown', () => {
25
+ const host = mountHost('button', {
26
+ 'data-long-press-duration': '100',
27
+ 'data-long-press-progress-interval': '20',
28
+ });
29
+ connectTrait(longPress, host);
30
+ pointerDown(host);
31
+ expect(host.hasAttribute('data-long-press-active')).toBe(true);
32
+ expect(host.getAttribute('data-long-press-progress')).toBe('0');
33
+ });
34
+
35
+ it('fires "long-press" after the configured duration elapses', () => {
36
+ const host = mountHost('button', {
37
+ 'data-long-press-duration': '100',
38
+ 'data-long-press-progress-interval': '20',
39
+ });
40
+ connectTrait(longPress, host);
41
+ const spy = spyEvent(host, 'long-press');
42
+ pointerDown(host);
43
+ vi.advanceTimersByTime(100);
44
+ expect(spy.count).toBe(1);
45
+ expect(host.hasAttribute('data-long-press-fired')).toBe(true);
46
+ });
47
+
48
+ it('emits "long-press-progress" at intervals while held', () => {
49
+ const host = mountHost('button', {
50
+ 'data-long-press-duration': '100',
51
+ 'data-long-press-progress-interval': '20',
52
+ });
53
+ connectTrait(longPress, host);
54
+ const spy = spyEvent(host, 'long-press-progress');
55
+ pointerDown(host);
56
+ vi.advanceTimersByTime(60); // 3 ticks at 20ms
57
+ expect(spy.count).toBeGreaterThanOrEqual(2);
58
+ // Each tick should report progress in [0, 1].
59
+ for (const detail of spy.captures) {
60
+ expect(detail.progress).toBeGreaterThanOrEqual(0);
61
+ expect(detail.progress).toBeLessThanOrEqual(1);
62
+ }
63
+ });
64
+
65
+ it('fires "long-press-cancelled" when pointerup arrives early', () => {
66
+ const host = mountHost('button', {
67
+ 'data-long-press-duration': '200',
68
+ 'data-long-press-progress-interval': '50',
69
+ });
70
+ connectTrait(longPress, host);
71
+ const fireSpy = spyEvent(host, 'long-press');
72
+ const cancelSpy = spyEvent(host, 'long-press-cancelled');
73
+ pointerDown(host);
74
+ vi.advanceTimersByTime(80); // well short of 200
75
+ pointerUp(host);
76
+ expect(fireSpy.count).toBe(0);
77
+ expect(cancelSpy.count).toBe(1);
78
+ expect(cancelSpy.last.reason).toBe('release');
79
+ });
80
+
81
+ it('fires "long-press-cancelled" on pointerleave', () => {
82
+ const host = mountHost('button', {
83
+ 'data-long-press-duration': '200',
84
+ });
85
+ connectTrait(longPress, host);
86
+ const cancelSpy = spyEvent(host, 'long-press-cancelled');
87
+ pointerDown(host);
88
+ vi.advanceTimersByTime(40);
89
+ pointerLeave(host);
90
+ expect(cancelSpy.count).toBe(1);
91
+ expect(cancelSpy.last.reason).toBe('leave');
92
+ expect(host.hasAttribute('data-long-press-active')).toBe(false);
93
+ });
94
+
95
+ it('cancels when pointermove exceeds the tolerance', () => {
96
+ const host = mountHost('button', {
97
+ 'data-long-press-duration': '300',
98
+ 'data-long-press-tolerance': '8',
99
+ });
100
+ connectTrait(longPress, host);
101
+ const cancelSpy = spyEvent(host, 'long-press-cancelled');
102
+ pointerDown(host, 0, 0);
103
+ pointerMove(host, 20, 0); // > 8px
104
+ expect(cancelSpy.count).toBe(1);
105
+ expect(cancelSpy.last.reason).toBe('move');
106
+ });
107
+
108
+ it('does NOT cancel when pointermove stays within tolerance', () => {
109
+ const host = mountHost('button', {
110
+ 'data-long-press-duration': '100',
111
+ 'data-long-press-tolerance': '8',
112
+ });
113
+ connectTrait(longPress, host);
114
+ const cancelSpy = spyEvent(host, 'long-press-cancelled');
115
+ const fireSpy = spyEvent(host, 'long-press');
116
+ pointerDown(host, 0, 0);
117
+ pointerMove(host, 5, 5); // hypot ~7.07, under 8
118
+ vi.advanceTimersByTime(100);
119
+ expect(cancelSpy.count).toBe(0);
120
+ expect(fireSpy.count).toBe(1);
121
+ });
122
+
123
+ it('respects [disabled] — no listeners fire, no attributes set', () => {
124
+ const host = mountHost('button', {
125
+ disabled: '',
126
+ 'data-long-press-duration': '50',
127
+ });
128
+ connectTrait(longPress, host);
129
+ const fireSpy = spyEvent(host, 'long-press');
130
+ pointerDown(host);
131
+ expect(host.hasAttribute('data-long-press-active')).toBe(false);
132
+ vi.advanceTimersByTime(100);
133
+ expect(fireSpy.count).toBe(0);
134
+ });
135
+
136
+ it('suppresses the synthetic click that follows a committed long-press', () => {
137
+ const host = mountHost('button', {
138
+ 'data-long-press-duration': '100',
139
+ });
140
+ connectTrait(longPress, host);
141
+ let clickHits = 0;
142
+ host.addEventListener('click', () => clickHits++);
143
+ pointerDown(host);
144
+ vi.advanceTimersByTime(100);
145
+ pointerUp(host);
146
+ host.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
147
+ expect(clickHits).toBe(0);
148
+ });
149
+
150
+ it('does NOT suppress click after a cancelled press', () => {
151
+ const host = mountHost('button', {
152
+ 'data-long-press-duration': '300',
153
+ });
154
+ connectTrait(longPress, host);
155
+ let clickHits = 0;
156
+ host.addEventListener('click', () => clickHits++);
157
+ pointerDown(host);
158
+ vi.advanceTimersByTime(60);
159
+ pointerUp(host); // early release → cancelled
160
+ host.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
161
+ expect(clickHits).toBe(1);
162
+ });
163
+
164
+ it('uses default duration (600ms) when attribute missing', () => {
165
+ const host = mountHost('button');
166
+ connectTrait(longPress, host);
167
+ const spy = spyEvent(host, 'long-press');
168
+ pointerDown(host);
169
+ vi.advanceTimersByTime(599);
170
+ expect(spy.count).toBe(0);
171
+ vi.advanceTimersByTime(2);
172
+ expect(spy.count).toBe(1);
173
+ });
174
+
175
+ it('uses default tolerance (8px) when attribute missing', () => {
176
+ const host = mountHost('button', { 'data-long-press-duration': '300' });
177
+ connectTrait(longPress, host);
178
+ const cancelSpy = spyEvent(host, 'long-press-cancelled');
179
+ pointerDown(host, 0, 0);
180
+ pointerMove(host, 7, 0); // under default 8
181
+ expect(cancelSpy.count).toBe(0);
182
+ pointerMove(host, 9, 0); // over default 8
183
+ expect(cancelSpy.count).toBe(1);
184
+ });
185
+
186
+ it('disconnect clears active timers + attributes mid-press', () => {
187
+ const host = mountHost('button', {
188
+ 'data-long-press-duration': '200',
189
+ 'data-long-press-progress-interval': '50',
190
+ });
191
+ const inst = connectTrait(longPress, host);
192
+ pointerDown(host);
193
+ expect(host.hasAttribute('data-long-press-active')).toBe(true);
194
+ inst.disconnect(host);
195
+ expect(host.hasAttribute('data-long-press-active')).toBe(false);
196
+ expect(host.hasAttribute('data-long-press-progress')).toBe(false);
197
+ // After disconnect, advancing time must not fire long-press.
198
+ const fireSpy = spyEvent(host, 'long-press');
199
+ vi.advanceTimersByTime(500);
200
+ expect(fireSpy.count).toBe(0);
201
+ });
202
+
203
+ it('reconnect after disconnect picks up fresh state', () => {
204
+ const host = mountHost('button', {
205
+ 'data-long-press-duration': '100',
206
+ });
207
+ const inst = connectTrait(longPress, host);
208
+ pointerDown(host);
209
+ inst.disconnect(host);
210
+
211
+ connectTrait(longPress, host);
212
+ const spy = spyEvent(host, 'long-press');
213
+ pointerDown(host);
214
+ vi.advanceTimersByTime(100);
215
+ expect(spy.count).toBe(1);
216
+ });
217
+
218
+ it('schema declares the input-interaction category and full attr/event/config sets', () => {
219
+ expect(longPress.schema.name).toBe('long-press');
220
+ expect(longPress.schema.category).toBe('input-interaction');
221
+ expect(longPress.schema.attributes).toContain('data-long-press-active');
222
+ expect(longPress.schema.attributes).toContain('data-long-press-progress');
223
+ expect(longPress.schema.attributes).toContain('data-long-press-fired');
224
+ expect(longPress.schema.events).toEqual(
225
+ expect.arrayContaining(['long-press', 'long-press-cancelled', 'long-press-progress']),
226
+ );
227
+ expect(longPress.schema.config).toEqual(
228
+ expect.arrayContaining([
229
+ 'data-long-press-duration',
230
+ 'data-long-press-tolerance',
231
+ 'data-long-press-progress-interval',
232
+ ]),
233
+ );
234
+ });
235
+
236
+ it('contextmenu is suppressed while a press is in flight', () => {
237
+ const host = mountHost('button', { 'data-long-press-duration': '200' });
238
+ connectTrait(longPress, host);
239
+ pointerDown(host);
240
+ const ev = new Event('contextmenu', { bubbles: true, cancelable: true });
241
+ host.dispatchEvent(ev);
242
+ expect(ev.defaultPrevented).toBe(true);
243
+ });
244
+ });
@@ -1,5 +1,5 @@
1
1
  import { defineTrait } from './define.js';
2
- import { prefersReducedMotion } from './_motion.js';
2
+ import { prefersReducedMotion } from './motion.js';
3
3
 
4
4
  export const magneticHover = defineTrait({
5
5
  name: 'magnetic-hover',
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { magneticHover } from './magnetic-hover.js';
3
- import { mountHost, connectTrait, resetDOM } from './_test-helpers.js';
3
+ import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
4
4
 
5
5
  describe('magnetic-hover', () => {
6
6
  beforeEach(resetDOM);
@@ -6,9 +6,14 @@ export const noiseTexture = defineTrait({
6
6
  description: 'Procedural grain overlay',
7
7
  attributes: ['data-noise-texture-active'],
8
8
  events: [],
9
- config: [],
9
+ config: ['data-noise-strength'],
10
10
  setup({ host }) {
11
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(#n)" opacity="0.08"/></svg>`;
11
+ // Strength governs the SVG rect's opacity (0..1). Default 0.15 visibly
12
+ // textured without dominating the underlying surface. Earlier defaults
13
+ // multiplied the rect opacity (0.08) by the overlay's CSS opacity (0.5)
14
+ // for an effective 0.04, which read as no-grain.
15
+ const strength = Math.max(0, Math.min(1, parseFloat(host.getAttribute('data-noise-strength')) || 0.15));
16
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(#n)" opacity="${strength}"/></svg>`;
12
17
  const encoded = `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
13
18
 
14
19
  const overlay = document.createElement('div');
@@ -17,7 +22,6 @@ export const noiseTexture = defineTrait({
17
22
  background-image: ${encoded};
18
23
  background-repeat: repeat;
19
24
  border-radius: inherit;
20
- opacity: 0.5;
21
25
  `;
22
26
 
23
27
  host.style.position = host.style.position || 'relative';
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { noiseTexture } from './noise-texture.js';
3
- import { mountHost, connectTrait, resetDOM } from './_test-helpers.js';
3
+ import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
4
4
 
5
5
  describe('noise-texture', () => {
6
6
  beforeEach(resetDOM);
@@ -1,5 +1,5 @@
1
1
  import { defineTrait } from './define.js';
2
- import { prefersReducedMotion } from './_motion.js';
2
+ import { prefersReducedMotion } from './motion.js';
3
3
 
4
4
  export const parallax = defineTrait({
5
5
  name: 'parallax',
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { parallax } from './parallax.js';
3
- import { mountHost, connectTrait, resetDOM } from './_test-helpers.js';
3
+ import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
4
4
 
5
5
  describe('parallax', () => {
6
6
  beforeEach(resetDOM);
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { portal } from './portal.js';
3
- import { mountHost, connectTrait, resetDOM } from './_test-helpers.js';
3
+ import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
4
4
 
5
5
  describe('portal', () => {
6
6
  beforeEach(resetDOM);
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { pressable } from './pressable.js';
3
- import { mountHost, connectTrait, spyEvent, resetDOM } from './_test-helpers.js';
3
+ import { mountHost, connectTrait, spyEvent, resetDOM } from './test-helpers.js';
4
4
 
5
5
  describe('pressable', () => {
6
6
  beforeEach(resetDOM);
@@ -17,12 +17,38 @@ export const resettable = defineTrait({
17
17
  events: ['reset-applied'],
18
18
  config: [],
19
19
  setup({ host }) {
20
- const initialValue = host.value ?? host.getAttribute('value') ?? '';
20
+ // Boolean controls (switch-ui, check-ui, radio-ui) carry their state on
21
+ // a `checked` reactive prop, not `value`. Detect at connect time so the
22
+ // captured initial + reset write target the right field.
23
+ //
24
+ // Native HTMLInputElement always exposes a `checked` getter regardless
25
+ // of type, so we can't bareword-check `'checked' in host`. Discriminate
26
+ // by tag name + native-input type. Custom AdiaUI elements only declare
27
+ // `checked` when it's their primary state — switch-ui, check-ui,
28
+ // radio-ui — so the bareword check is safe there.
29
+ const tag = host.tagName?.toLowerCase?.() || '';
30
+ const type = host.getAttribute?.('type') || '';
31
+ const isNativeInput = tag === 'input';
32
+ const isNativeCheckable = isNativeInput && (type === 'checkbox' || type === 'radio');
33
+ const isCustomCheckable = !isNativeInput
34
+ && 'checked' in host
35
+ && typeof host.checked === 'boolean';
36
+ const isCheckable = isNativeCheckable || isCustomCheckable;
37
+
38
+ const initialValue = isCheckable
39
+ ? host.checked
40
+ : (host.value ?? host.getAttribute('value') ?? '');
41
+
21
42
  const form = host.closest?.('form');
22
43
 
23
44
  function onReset() {
24
- if ('value' in host) host.value = initialValue;
25
- else host.setAttribute('value', initialValue);
45
+ if (isCheckable) {
46
+ host.checked = initialValue;
47
+ } else if ('value' in host) {
48
+ host.value = initialValue;
49
+ } else {
50
+ host.setAttribute('value', initialValue);
51
+ }
26
52
  host.dispatchEvent(new CustomEvent('reset-applied', {
27
53
  bubbles: true,
28
54
  detail: { initialValue },
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { resettable } from './resettable.js';
3
- import { mountHost, connectTrait, spyEvent, resetDOM } from './_test-helpers.js';
3
+ import { mountHost, connectTrait, spyEvent, resetDOM } from './test-helpers.js';
4
4
 
5
5
  function mountInForm(initialValue = 'initial') {
6
6
  const form = document.createElement('form');
@@ -64,4 +64,37 @@ describe('resettable', () => {
64
64
  expect(a.value).toBe('A');
65
65
  expect(b.value).toBe('B');
66
66
  });
67
+
68
+ it('boolean controls (host.checked) restore their initial checked state', () => {
69
+ // Synthesize a switch-like host: the only state is a boolean `checked`
70
+ // reactive property — no `value`. Mirrors switch-ui's API.
71
+ const form = document.createElement('form');
72
+ document.body.appendChild(form);
73
+ const host = document.createElement('div');
74
+ host.checked = true;
75
+ form.appendChild(host);
76
+
77
+ connectTrait(resettable, host);
78
+ const spy = spyEvent(host, 'reset-applied');
79
+
80
+ host.checked = false; // user toggles it off
81
+ form.dispatchEvent(new Event('reset', { bubbles: true }));
82
+
83
+ expect(host.checked).toBe(true);
84
+ expect(spy.count).toBe(1);
85
+ expect(spy.last.initialValue).toBe(true);
86
+ });
87
+
88
+ it('boolean controls starting unchecked stay unchecked after reset', () => {
89
+ const form = document.createElement('form');
90
+ document.body.appendChild(form);
91
+ const host = document.createElement('div');
92
+ host.checked = false;
93
+ form.appendChild(host);
94
+
95
+ connectTrait(resettable, host);
96
+ host.checked = true;
97
+ form.dispatchEvent(new Event('reset', { bubbles: true }));
98
+ expect(host.checked).toBe(false);
99
+ });
67
100
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { resizable } from './resizable.js';
3
- import { mountHost, connectTrait, resetDOM } from './_test-helpers.js';
3
+ import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
4
4
 
5
5
  describe('resizable', () => {
6
6
  beforeEach(resetDOM);
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
2
  import { resizeObserver } from './resize-observer.js';
3
- import { mountHost, connectTrait, resetDOM } from './_test-helpers.js';
3
+ import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
4
4
 
5
5
  describe('resize-observer', () => {
6
6
  let originalRO;
package/traits/ripple.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineTrait } from './define.js';
2
- import { prefersReducedMotion } from './_motion.js';
2
+ import { prefersReducedMotion } from './motion.js';
3
3
 
4
4
  export const ripple = defineTrait({
5
5
  name: 'ripple',
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { ripple } from './ripple.js';
3
- import { mountHost, connectTrait, resetDOM } from './_test-helpers.js';
3
+ import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
4
4
 
5
5
  describe('ripple', () => {
6
6
  beforeEach(resetDOM);
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { rovingTabindex } from './roving-tabindex.js';
3
- import { mountHost, connectTrait, resetDOM } from './_test-helpers.js';
3
+ import { mountHost, connectTrait, resetDOM } from './test-helpers.js';
4
4
 
5
5
  function tabbableChild(host) {
6
6
  const btn = document.createElement('button');